update
This commit is contained in:
1 parent
09971cabc2
commit
9145572422
163 files changed
+3348
-2874
No files matched your search
@@ -1,2 +1,2 @@
|
||||
// Backward-compatible re-exports from decomposed onboarding components
|
||||
export { CompanyOnboarding } from "./CompanyOnboarding/OnboardingWizard";
|
||||
export { CompanyOnboarding } from "./company-onboarding/OnboardingWizard";
|
||||
@@ -1,6 +1 @@
|
||||
// Backward-compatible re-exports from decomposed automation components
|
||||
export { AutomationPanel } from "./automation/AutomationPanel"
|
||||
export { RuleCard } from "./automation/RuleCard"
|
||||
export { RuleFormModal } from "./automation/RuleEditor"
|
||||
export { WorkflowCard, StepTimeline } from "./automation/WorkflowList"
|
||||
export { WorkflowFormModal } from "./automation/WorkflowEditor"
|
||||
@@ -1,9 +1 @@
|
||||
// Backward-compatible re-exports from decomposed communication components
|
||||
export { CommunicationHub } from "./communication/CommunicationHub"
|
||||
export { ChannelSidebar, FilterBar } from "./communication/ChannelSidebar"
|
||||
export { MessageList } from "./communication/MessageList"
|
||||
export { MessageBubble, TaskCommentBubble, AgentAvatar } from "./communication/MessageItem"
|
||||
export { MessageComposer, TaskDiscussionComposer, NoChannelComposer } from "./communication/MessageComposer"
|
||||
export { ChannelHeader } from "./communication/ChannelHeader"
|
||||
export type { ChannelDescriptor, TaskDiscussionDescriptor, AnyChannel } from "./communication/ChannelHeader"
|
||||
export type { FilterState } from "./communication/ChannelSidebar"
|
||||
@@ -114,7 +114,7 @@ function AttachmentPicker({ documents, attachments, onAdd, onRemove }: {
|
||||
function MentionDropdown({ agents, query, onSelect, position }: {
|
||||
agents: AgentRecord[];
|
||||
query: string;
|
||||
onSelect: (agent: AgentRecord) => void;
|
||||
onSelect: (agent: AgentRecord | null) => void;
|
||||
position: { top: number; left: number };
|
||||
}) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
@@ -136,7 +136,7 @@ function MentionDropdown({ agents, query, onSelect, position }: {
|
||||
else if (e.key === "Enter" || e.key === "Tab") {
|
||||
if (filtered[selectedIndex]) { e.preventDefault(); onSelect(filtered[selectedIndex]) }
|
||||
}
|
||||
else if (e.key === "Escape") { onSelect(null as unknown as AgentRecord) }
|
||||
else if (e.key === "Escape") { onSelect(null) }
|
||||
}
|
||||
document.addEventListener("keydown", handler, true)
|
||||
return () => document.removeEventListener("keydown", handler, true)
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function WaitStateIndicator({ waitState, onReply, onNavigateAppro
|
||||
</div>
|
||||
{isApproval && onNavigateApproval ? (
|
||||
<button
|
||||
onClick={() => onNavigateApproval(waitState.relatedApprovalId!)}
|
||||
onClick={() => { const id = waitState.relatedApprovalId; if (id) onNavigateApproval(id); }}
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-md bg-amber-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-700 dark:bg-amber-500 dark:hover:bg-amber-600"
|
||||
>
|
||||
<CheckCircle size={14} />
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+7
-6
@@ -7,27 +7,28 @@ import {
|
||||
Terminal,
|
||||
} from "@phosphor-icons/react";
|
||||
import { ActionButton } from "../ui";
|
||||
import type { TerminalCommandKey } from "@shared/contracts";
|
||||
|
||||
interface NodeCheckStepProps {
|
||||
nodeStatus: { installed: boolean; version: string | null } | null;
|
||||
checking: boolean;
|
||||
onCheck: () => void;
|
||||
onInstall: (command: string) => void;
|
||||
onInstall: (commandKey: TerminalCommandKey) => void;
|
||||
onContinue: () => void;
|
||||
onBack: () => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
const INSTALL_COMMANDS: Record<string, { label: string; code: string; command: string }> = {
|
||||
const INSTALL_COMMANDS: Record<string, { label: string; code: string; commandKey: TerminalCommandKey }> = {
|
||||
darwin: {
|
||||
label: "macOS",
|
||||
code: "brew install node@22",
|
||||
command: "brew install node@22",
|
||||
commandKey: "install_node_darwin",
|
||||
},
|
||||
win32: {
|
||||
label: "Windows",
|
||||
code: "winget install -e --id OpenJS.NodeJS.LTS",
|
||||
command: "winget install -e --id OpenJS.NodeJS.LTS",
|
||||
commandKey: "install_node_win32",
|
||||
},
|
||||
linux: {
|
||||
label: "Linux",
|
||||
@@ -42,7 +43,7 @@ nvm install 22
|
||||
|
||||
# Verify the Node.js version:
|
||||
node -v`,
|
||||
command: `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash && \\. "$HOME/.nvm/nvm.sh" && nvm install 22`,
|
||||
commandKey: "install_node_linux",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,7 +110,7 @@ export function NodeCheckStep({
|
||||
</pre>
|
||||
<ActionButton
|
||||
label={t("onboarding.installNodeTerminal")}
|
||||
onClick={() => onInstall(platformInfo.command)}
|
||||
onClick={() => onInstall(platformInfo.commandKey)}
|
||||
icon={Terminal}
|
||||
/>
|
||||
</div>
|
||||
File renamed without changes.
+3
-2
@@ -1,6 +1,7 @@
|
||||
import type { AppLocale } from "@shared/locale";
|
||||
import type { ConnectorId, OnboardingBootstrapResult, ProfileSnapshot } from "@shared/types";
|
||||
import type { ConnectorModelEntry } from "@shared/connector-models";
|
||||
import type { TerminalCommandKey } from "@shared/contracts";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import type { TranslationFn } from "../../i18n";
|
||||
import { OnboardingHero, type OnboardingStep } from "./OnboardingHero";
|
||||
@@ -22,7 +23,7 @@ interface OnboardingStepPanelProps {
|
||||
nodeStatus: { installed: boolean; version: string | null } | null;
|
||||
nodeChecking: boolean;
|
||||
onCheckNode: () => void;
|
||||
onInstallCommand: (title: string, command: string) => void;
|
||||
onInstallCommand: (commandKey: TerminalCommandKey) => void;
|
||||
connectors: ProfileSnapshot["connectors"];
|
||||
hasAtLeastOneReady: boolean;
|
||||
onTestConnector: (connectorId: string) => void;
|
||||
@@ -148,7 +149,7 @@ export function OnboardingStepPanel({
|
||||
nodeStatus={nodeStatus}
|
||||
checking={nodeChecking}
|
||||
onCheck={onCheckNode}
|
||||
onInstall={(command) => onInstallCommand("Install Node.js", command)}
|
||||
onInstall={(commandKey) => onInstallCommand(commandKey)}
|
||||
onContinue={onContinueFromNodeCheck}
|
||||
onBack={() => onBack("welcome")}
|
||||
t={t}
|
||||
+1
-1
@@ -189,7 +189,7 @@ export function CompanyOnboarding({
|
||||
nodeStatus={nodeStatus}
|
||||
nodeChecking={nodeChecking}
|
||||
onCheckNode={() => void checkNode()}
|
||||
onInstallCommand={(title, command) => void handleInstallCommand(title, command)}
|
||||
onInstallCommand={(commandKey) => void handleInstallCommand(commandKey)}
|
||||
connectors={connectors}
|
||||
hasAtLeastOneReady={hasAtLeastOneReady}
|
||||
onTestConnector={(id) => void handleTestConnector(id)}
|
||||
+8
-4
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight, FloppyDisk, SignIn } from "@phosphor-icons/react";
|
||||
import type { ProfileSnapshot } from "@shared/types";
|
||||
import type { TerminalCommandKey } from "@shared/contracts";
|
||||
import { ActionButton } from "../ui";
|
||||
import {
|
||||
CompatProviderCard,
|
||||
@@ -20,7 +21,7 @@ interface ProvidersStepProps {
|
||||
onTestConnector: (connectorId: string) => void;
|
||||
onSaveConnector: (connectorId: string, envBindingText: string, model: string | null, notes?: string) => void;
|
||||
onLoginConnector: (connectorId: string) => void;
|
||||
onInstallCommand: (title: string, command: string) => void;
|
||||
onInstallCommand: (commandKey: TerminalCommandKey) => void;
|
||||
hasAtLeastOneReady: boolean;
|
||||
onContinue: () => void;
|
||||
onBack: () => void;
|
||||
@@ -125,8 +126,9 @@ export function ProvidersStep({
|
||||
<InstallBlock
|
||||
macLinuxCmd="curl -fsSL https://claude.ai/install.sh | bash"
|
||||
windowsCmd="irm https://claude.ai/install.ps1 | iex"
|
||||
commandKeyUnix="install_claude_unix"
|
||||
commandKeyWin32="install_claude_win32"
|
||||
onInstall={onInstallCommand}
|
||||
title="Install Claude Code"
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
@@ -171,8 +173,9 @@ export function ProvidersStep({
|
||||
<InstallBlock
|
||||
macLinuxCmd="npm i -g @openai/codex"
|
||||
windowsCmd={null}
|
||||
commandKeyUnix="install_codex"
|
||||
commandKeyWin32={null}
|
||||
onInstall={onInstallCommand}
|
||||
title="Install Codex"
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
@@ -217,8 +220,9 @@ export function ProvidersStep({
|
||||
<InstallBlock
|
||||
macLinuxCmd="npm install -g @google/gemini-cli"
|
||||
windowsCmd={null}
|
||||
commandKeyUnix="install_gemini"
|
||||
commandKeyWin32={null}
|
||||
onInstall={onInstallCommand}
|
||||
title="Install Gemini CLI"
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
+11
-9
@@ -7,6 +7,7 @@ import {
|
||||
Terminal,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { ProfileSnapshot } from "@shared/types";
|
||||
import type { TerminalCommandKey } from "@shared/contracts";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Compatible-API provider presets */
|
||||
@@ -15,8 +16,6 @@ import type { ProfileSnapshot } from "@shared/types";
|
||||
export const COMPATIBLE_PROVIDERS = [
|
||||
{ id: "minimax", label: "MiniMax", baseUrl: "https://api.minimax.io/anthropic", docUrl: "https://platform.minimax.io/docs/token-plan/claude-code" },
|
||||
{ id: "minimax_cn", label: "MiniMax (CN)", baseUrl: "https://api.minimaxi.com/anthropic", docUrl: "https://platform.minimax.io/docs/token-plan/claude-code" },
|
||||
{ id: "kimi", label: "Kimi", baseUrl: "https://api.kimi.com/coding/", docUrl: "https://www.kimi.com/code/docs/en/more/third-party-agents.html" },
|
||||
{ id: "kimi_cn", label: "Kimi CN", baseUrl: "https://api.moonshot.cn/anthropic", docUrl: "https://platform.moonshot.cn/docs/guide/agent-support#macos-%E5%92%8C-linux-1" },
|
||||
{ id: "glm", label: "GLM (ZhiPu)", baseUrl: "https://api.z.ai/api/anthropic", docUrl: "https://docs.z.ai/devpack/tool/claude" },
|
||||
{ id: "openrouter", label: "OpenRouter", baseUrl: "https://openrouter.ai/api", docUrl: "https://openrouter.ai/docs/guides/coding-agents/claude-code-integration" },
|
||||
{ id: "tencent", label: "Tencent Coding Plan", baseUrl: "https://api.lkeap.cloud.tencent.com/coding/anthropic", docUrl: "https://cloud.tencent.com/document/product/1772/128951" },
|
||||
@@ -135,19 +134,21 @@ export function ProviderCard({
|
||||
export function InstallBlock({
|
||||
macLinuxCmd,
|
||||
windowsCmd,
|
||||
commandKeyUnix,
|
||||
commandKeyWin32,
|
||||
onInstall,
|
||||
title,
|
||||
t,
|
||||
}: {
|
||||
macLinuxCmd: string;
|
||||
windowsCmd: string | null;
|
||||
onInstall: (title: string, command: string) => void;
|
||||
title: string;
|
||||
commandKeyUnix: TerminalCommandKey;
|
||||
commandKeyWin32: TerminalCommandKey | null;
|
||||
onInstall: (commandKey: TerminalCommandKey) => void;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const platform = window.agentCompany?.platform ?? "linux";
|
||||
const isWindows = platform === "win32";
|
||||
const cmd = isWindows && windowsCmd ? windowsCmd : macLinuxCmd;
|
||||
const commandKey = isWindows && commandKeyWin32 ? commandKeyWin32 : commandKeyUnix;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
@@ -164,7 +165,7 @@ export function InstallBlock({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onInstall(title, cmd)}
|
||||
onClick={() => onInstall(commandKey)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-[12px] font-medium text-foreground transition hover:bg-muted"
|
||||
>
|
||||
<Terminal size={14} />
|
||||
@@ -192,7 +193,7 @@ export function CompatProviderCard({
|
||||
connector: ProfileSnapshot["connectors"][number] | null;
|
||||
claudeInstalled: boolean;
|
||||
onSaveConnector: ProvidersStepSaveConnector;
|
||||
onInstallCommand: (title: string, command: string) => void;
|
||||
onInstallCommand: (commandKey: TerminalCommandKey) => void;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
t: (key: string) => string;
|
||||
@@ -266,8 +267,9 @@ export function CompatProviderCard({
|
||||
<InstallBlock
|
||||
macLinuxCmd="curl -fsSL https://claude.ai/install.sh | bash"
|
||||
windowsCmd="irm https://claude.ai/install.ps1 | iex"
|
||||
commandKeyUnix="install_claude_unix"
|
||||
commandKeyWin32="install_claude_win32"
|
||||
onInstall={onInstallCommand}
|
||||
title="Install Claude Code"
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
File renamed without changes.
+4
-2
@@ -1,6 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import type { ConnectorId } from "@shared/types";
|
||||
import type { ProfileSnapshot } from "@shared/types";
|
||||
import type { openTerminalCommandSchema } from "@shared/contracts";
|
||||
import type { z } from "zod";
|
||||
import { unwrap } from "../../lib/desktop";
|
||||
|
||||
export function useConnectorCallbacks(connectors: ProfileSnapshot["connectors"]) {
|
||||
@@ -48,9 +50,9 @@ export function useConnectorCallbacks(connectors: ProfileSnapshot["connectors"])
|
||||
}, 5000);
|
||||
}, []);
|
||||
|
||||
const handleInstallCommand = useCallback(async (title: string, command: string) => {
|
||||
const handleInstallCommand = useCallback(async (commandKey: z.infer<typeof openTerminalCommandSchema>["commandKey"]) => {
|
||||
try {
|
||||
unwrap(await window.agentCompany.openTerminalCommand({ title, command }));
|
||||
unwrap(await window.agentCompany.openTerminalCommand({ commandKey }));
|
||||
} catch {
|
||||
// Terminal open failure is non-fatal
|
||||
return;
|
||||
File renamed without changes.
@@ -5,6 +5,7 @@ import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, M
|
||||
import { useT } from "../../i18n";
|
||||
import { documentTypeOptions, documentStatusOptions, splitTags, parseTags } from "./helpers";
|
||||
import { SectionHeader } from "./SectionHeader";
|
||||
import { NEW_ENTITY_ID } from "../../lib/constants";
|
||||
|
||||
function DocumentEditor({ companyId, agents, projects, goals, document, onSaved }: {
|
||||
companyId: string; agents: AgentRecord[]; projects: ProjectRecord[]; goals: GoalRecord[];
|
||||
@@ -75,7 +76,7 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader title={t("ops.documentCenter")} count={documents.length} action={() => setEditingId("__new__")} />
|
||||
<SectionHeader title={t("ops.documentCenter")} count={documents.length} action={() => setEditingId(NEW_ENTITY_ID)} />
|
||||
<ListFrame>
|
||||
{documents.map((document) => (
|
||||
<ListRow key={document.id}>
|
||||
@@ -97,7 +98,7 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
|
||||
))}
|
||||
</ListFrame>
|
||||
<Modal open={editingId !== null} title={activeDocument ? activeDocument.title : "New document"} onClose={() => setEditingId(null)} width="760px">
|
||||
<DocumentEditor companyId={companyId} agents={agents} projects={projects} goals={goals} document={editingId === "__new__" ? null : activeDocument} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
<DocumentEditor companyId={companyId} agents={agents} projects={projects} goals={goals} document={editingId === NEW_ENTITY_ID ? null : activeDocument} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
</Modal>
|
||||
<ConfirmDialog open={deleteId !== null} title="Delete document" message="This removes the document from the company knowledge base." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteDocument({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, M
|
||||
import { useT } from "../../i18n";
|
||||
import { knowledgeCategoryOptions, importanceOptions, splitTags, parseTags } from "./helpers";
|
||||
import { SectionHeader } from "./SectionHeader";
|
||||
import { NEW_ENTITY_ID } from "../../lib/constants";
|
||||
|
||||
function KnowledgeEditor({ companyId, entry, onSaved }: {
|
||||
companyId: string; entry: KnowledgeEntryRecord | null; onSaved: () => Promise<void>;
|
||||
@@ -63,7 +64,7 @@ export function KnowledgeBaseManager({ companyId, entries, onRefresh }: {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader title={t("ops.knowledgeBase")} count={entries.length} action={() => setEditingId("__new__")} />
|
||||
<SectionHeader title={t("ops.knowledgeBase")} count={entries.length} action={() => setEditingId(NEW_ENTITY_ID)} />
|
||||
<ListFrame>
|
||||
{entries.map((entry) => (
|
||||
<ListRow key={entry.id}>
|
||||
@@ -85,7 +86,7 @@ export function KnowledgeBaseManager({ companyId, entries, onRefresh }: {
|
||||
))}
|
||||
</ListFrame>
|
||||
<Modal open={editingId !== null} title={activeEntry ? activeEntry.topic : "New knowledge entry"} onClose={() => setEditingId(null)} width="760px">
|
||||
<KnowledgeEditor companyId={companyId} entry={editingId === "__new__" ? null : activeEntry} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
<KnowledgeEditor companyId={companyId} entry={editingId === NEW_ENTITY_ID ? null : activeEntry} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
</Modal>
|
||||
<ConfirmDialog open={deleteId !== null} title="Delete knowledge entry" message="This will permanently delete this entry." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteKnowledgeEntry({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, M
|
||||
import { useT } from "../../i18n";
|
||||
import { meetingTypeOptions, meetingStatusOptions, splitTags, parseTags, parseParticipantIds, formatParticipantIds } from "./helpers";
|
||||
import { SectionHeader } from "./SectionHeader";
|
||||
import { NEW_ENTITY_ID } from "../../lib/constants";
|
||||
|
||||
function MeetingEditor({ companyId, agents, meeting, onSaved }: {
|
||||
companyId: string; agents: AgentRecord[]; meeting: MeetingRecord | null; onSaved: () => Promise<void>;
|
||||
@@ -77,7 +78,7 @@ export function MeetingsManager({ companyId, meetings, agents, onRefresh }: {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader title={t("ops.meetings")} count={meetings.length} action={() => setEditingId("__new__")} />
|
||||
<SectionHeader title={t("ops.meetings")} count={meetings.length} action={() => setEditingId(NEW_ENTITY_ID)} />
|
||||
<ListFrame>
|
||||
{meetings.map((meeting) => (
|
||||
<ListRow key={meeting.id}>
|
||||
@@ -99,7 +100,7 @@ export function MeetingsManager({ companyId, meetings, agents, onRefresh }: {
|
||||
))}
|
||||
</ListFrame>
|
||||
<Modal open={editingId !== null} title={activeMeeting ? activeMeeting.title : "New meeting"} onClose={() => setEditingId(null)} width="760px">
|
||||
<MeetingEditor companyId={companyId} agents={agents} meeting={editingId === "__new__" ? null : activeMeeting} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
<MeetingEditor companyId={companyId} agents={agents} meeting={editingId === NEW_ENTITY_ID ? null : activeMeeting} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
</Modal>
|
||||
<ConfirmDialog open={deleteId !== null} title="Delete meeting" message="This removes the meeting and its operational context from the company timeline." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteMeeting({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, M
|
||||
import { useT } from "../../i18n";
|
||||
import { sprintStatusOptions } from "./helpers";
|
||||
import { SectionHeader } from "./SectionHeader";
|
||||
import { NEW_ENTITY_ID } from "../../lib/constants";
|
||||
|
||||
function SprintEditor({ companyId, sprint, onSaved }: {
|
||||
companyId: string; sprint: SprintRecord | null; onSaved: () => Promise<void>;
|
||||
@@ -69,7 +70,7 @@ export function SprintManager({ companyId, sprints, onRefresh }: {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader title={t("ops.sprintBoard")} count={sprints.length} action={() => setEditingId("__new__")} />
|
||||
<SectionHeader title={t("ops.sprintBoard")} count={sprints.length} action={() => setEditingId(NEW_ENTITY_ID)} />
|
||||
<ListFrame>
|
||||
{sprints.map((sprint) => (
|
||||
<ListRow key={sprint.id}>
|
||||
@@ -91,7 +92,7 @@ export function SprintManager({ companyId, sprints, onRefresh }: {
|
||||
))}
|
||||
</ListFrame>
|
||||
<Modal open={editingId !== null} title={activeSprint ? activeSprint.name : "New sprint"} onClose={() => setEditingId(null)} width="720px">
|
||||
<SprintEditor companyId={companyId} sprint={editingId === "__new__" ? null : activeSprint} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
<SprintEditor companyId={companyId} sprint={editingId === NEW_ENTITY_ID ? null : activeSprint} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
</Modal>
|
||||
<ConfirmDialog open={deleteId !== null} title="Delete sprint" message="This removes the sprint board entry. Historical run and task records remain." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteSprint({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
|
||||
</div>
|
||||
|
||||
@@ -85,15 +85,15 @@ export function Dashboard(props: DashboardProps) {
|
||||
<div className="mt-2 flex items-center gap-3 text-[13px] tracking-[0.005em] text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-success" />
|
||||
{activeAgents.length} {activeAgents.length === 1 ? "agent" : "agents"} active
|
||||
{activeAgents.length} {activeAgents.length === 1 ? t("dash.agentActive") : t("dash.agentsActive")}
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--border)]" />
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
{activeTasks.length} {activeTasks.length === 1 ? "task" : "tasks"} in progress
|
||||
{activeTasks.length} {activeTasks.length === 1 ? t("dash.taskInProgress") : t("dash.tasksInProgress")}
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--border)]" />
|
||||
<span>{tasks.filter((tk) => tk.status === "done").length} completed</span>
|
||||
<span>{tasks.filter((tk) => tk.status === "done").length} {t("dash.completedCount")}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -121,12 +121,12 @@ export function Dashboard(props: DashboardProps) {
|
||||
subtitle={pendingApprovals.length > 0 ? t("dash.needsAttention") : t("dash.allClear")}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Agents"
|
||||
label={t("common.agents")}
|
||||
value={activeAgents.length}
|
||||
icon={Robot}
|
||||
tone={activeAgents.filter((a) => a.status === "running").length > 0 ? "success" : "default"}
|
||||
onClick={() => onNavigate("agents")}
|
||||
subtitle={`${activeAgents.filter((a) => a.status === "running").length} running`}
|
||||
subtitle={`${activeAgents.filter((a) => a.status === "running").length} ${t("dash.running")}`}
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("dash.totalSpend")}
|
||||
@@ -225,10 +225,10 @@ export function Dashboard(props: DashboardProps) {
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Active Work
|
||||
{t("dash.activeWork")}
|
||||
</div>
|
||||
<button type="button" onClick={() => onNavigate("runs")} className="flex items-center gap-1 text-[11px] font-medium text-primary transition hover:underline">
|
||||
All runs <ArrowRight size={12} />
|
||||
{t("dash.allRuns")} <ArrowRight size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{activeWork.length > 0 ? (
|
||||
@@ -263,7 +263,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-border py-8 text-center">
|
||||
<div className="text-[13px] text-muted-foreground">{t("dash.allQuiet")}</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">No tasks currently running</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">{t("dash.noTasksRunning")}</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Warning, ArrowClockwise, X, Trash } from "@phosphor-icons/react";
|
||||
import type { DesktopEvent } from "@shared/types/events";
|
||||
|
||||
interface InterruptedRun {
|
||||
runId: string;
|
||||
@@ -16,7 +17,7 @@ export function RecoveryBanner() {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.agentCompany?.subscribe?.((event: any) => {
|
||||
const unsubscribe = window.agentCompany?.subscribe?.((event: DesktopEvent) => {
|
||||
if (event.type === "recovery-needed" && event.interruptedRuns) {
|
||||
setRuns(event.interruptedRuns);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ export { BudgetSummary } from "./execution/BudgetSummary"
|
||||
export { WorkspaceList } from "./execution/WorkspaceList"
|
||||
export { WorkspaceInspector } from "./execution/WorkspaceInspector"
|
||||
export { WorkspaceCreator } from "./execution/WorkspaceCreator"
|
||||
export { SecretCreator } from "./execution/SecretCreator"
|
||||
export { SettingsPane } from "./execution/SettingsPane"
|
||||
export { ModelManagement } from "./execution/ModelManagement"
|
||||
export { PendingReviewList } from "./execution/PendingReviewList"
|
||||
|
||||
@@ -104,7 +104,7 @@ function ApprovalDocumentsPanel({
|
||||
reference,
|
||||
document: documents.find((entry) => entry.id === reference.id) ?? null,
|
||||
}))
|
||||
.filter((entry) => entry.document)
|
||||
.filter((entry): entry is typeof entry & { document: NonNullable<typeof entry.document> } => entry.document != null)
|
||||
: []
|
||||
|
||||
return (
|
||||
@@ -112,9 +112,9 @@ function ApprovalDocumentsPanel({
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Linked Documents</div>
|
||||
{fullDocs.length > 0 ? fullDocs.map(({ reference, document }) => (
|
||||
<ApprovalDocumentEditor
|
||||
key={document!.id}
|
||||
key={document.id}
|
||||
companyId={companyId}
|
||||
document={document!}
|
||||
document={document}
|
||||
fallback={reference}
|
||||
/>
|
||||
)) : references.map((reference) => (
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
import { CaretDown, CaretRight } from "@phosphor-icons/react";
|
||||
import type { ProfileSnapshot } from "@shared/types";
|
||||
import { COMPATIBLE_PROVIDERS } from "../company-onboarding/ProvidersStepShared";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Compatible-API provider presets (mirrors ProvidersStep) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const COMPATIBLE_PROVIDERS = [
|
||||
{ id: "minimax", label: "MiniMax", baseUrl: "https://api.minimax.io/anthropic", docUrl: "https://platform.minimax.io/docs/token-plan/claude-code" },
|
||||
{ id: "minimax_cn", label: "MiniMax (CN)", baseUrl: "https://api.minimaxi.com/anthropic", docUrl: "https://platform.minimax.io/docs/token-plan/claude-code" },
|
||||
{ id: "kimi", label: "Kimi", baseUrl: "https://api.kimi.com/coding/", docUrl: "https://www.kimi.com/code/docs/en/more/third-party-agents.html" },
|
||||
{ id: "kimi_cn", label: "Kimi CN", baseUrl: "https://api.moonshot.cn/anthropic", docUrl: "https://platform.moonshot.cn/docs/guide/agent-support" },
|
||||
{ id: "glm", label: "GLM (ZhiPu)", baseUrl: "https://api.z.ai/api/anthropic", docUrl: "https://docs.z.ai/devpack/tool/claude" },
|
||||
{ id: "openrouter", label: "OpenRouter", baseUrl: "https://openrouter.ai/api", docUrl: "https://openrouter.ai/docs/guides/coding-agents/claude-code-integration" },
|
||||
{ id: "tencent", label: "Tencent Coding Plan", baseUrl: "https://api.lkeap.cloud.tencent.com/coding/anthropic", docUrl: "https://cloud.tencent.com/document/product/1772/128951" },
|
||||
{ id: "poe", label: "POE", baseUrl: "https://api.poe.com", docUrl: "https://creator.poe.com/docs/external-applications/claude-code" },
|
||||
{ id: "deepseek", label: "DeepSeek", baseUrl: "https://api.deepseek.com/anthropic", docUrl: "https://api-docs.deepseek.com/guides/anthropic_api" },
|
||||
{ id: "alibaba", label: "Alibaba Coding Plan", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic", docUrl: "https://www.alibabacloud.com/help/en/model-studio/claude-code-coding-plan" },
|
||||
{ id: "alibaba_cn", label: "Alibaba Coding Plan (CN)", baseUrl: "https://coding.dashscope.aliyuncs.com/apps/anthropic", docUrl: "https://help.aliyun.com/zh/model-studio/claude-code-coding-plan" },
|
||||
{ id: "bailing", label: "BaiLing", baseUrl: "https://api.tbox.cn/api/anthropic", docUrl: "https://alipaytbox.yuque.com/sxs0ba/ling/best_practices_claudecode" },
|
||||
{ id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", docUrl: "https://docs.nano-gpt.com/integrations/claude-code#manual-configuration-all-platforms" },
|
||||
{ id: "zhipu_cn", label: "ZhiPu (CN)", baseUrl: "https://open.bigmodel.cn/api/anthropic", docUrl: "https://docs.bigmodel.cn/cn/coding-plan/tool/claude" },
|
||||
{ id: "ollama", label: "Ollama (Local)", baseUrl: "http://localhost:11434", docUrl: "https://docs.ollama.com/integrations/claude-code" },
|
||||
{ id: "custom", label: "Custom", baseUrl: "", docUrl: "" },
|
||||
] as const;
|
||||
export { COMPATIBLE_PROVIDERS };
|
||||
|
||||
export const COMPAT_SLOTS = [
|
||||
"claude_compatible", "claude_compat_2", "claude_compat_3", "claude_compat_4", "claude_compat_5",
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useState } from "react"
|
||||
import { ShieldCheck } from "@phosphor-icons/react"
|
||||
import { useT } from "../../i18n"
|
||||
import { unwrap } from "../../lib/desktop"
|
||||
import { ActionButton, InlineNotice, Input, TextArea } from "../ui"
|
||||
import { describeActionError } from "./execution-utils"
|
||||
|
||||
export function SecretCreator({ companyId, onSaved }: { companyId: string; onSaved: () => Promise<void> }) {
|
||||
const t = useT()
|
||||
const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [value, setValue] = useState("")
|
||||
const [errors, setErrors] = useState<{ name?: string; value?: string }>({}); const [submitError, setSubmitError] = useState<string | null>(null); const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
function validate(): boolean {
|
||||
const next: typeof errors = {}
|
||||
if (!name.trim()) next.name = "Secret name is required."
|
||||
else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name.trim())) next.name = "Name must be alphanumeric with underscores, starting with a letter or underscore."
|
||||
if (!value.trim()) next.value = "Secret value cannot be empty."
|
||||
setErrors(next); return Object.keys(next).length === 0
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div><Input label={t("exec.secretNameLabel")} value={name} onChange={(v) => { setName(v); if (errors.name) setErrors((e) => ({ ...e, name: undefined })) }} placeholder="e.g. ANTHROPIC_API_KEY" />{errors.name ? <div className="mt-1 text-[12px] text-destructive" role="alert">{errors.name}</div> : null}</div>
|
||||
<Input label="Description" value={description} onChange={setDescription} placeholder="What this secret is used for" />
|
||||
<div><TextArea label="Secret value" value={value} onChange={(v) => { setValue(v); if (errors.value) setErrors((e) => ({ ...e, value: undefined })) }} rows={5} placeholder="Paste the secret value" />{errors.value ? <div className="mt-1 text-[12px] text-destructive" role="alert">{errors.value}</div> : null}</div>
|
||||
{submitError ? <InlineNotice message={submitError} /> : null}
|
||||
<ActionButton label={t("exec.storeSecret")} tone="accent" onClick={() => void (async () => { if (!validate()) return; setSubmitError(null); setSubmitting(true); try { unwrap(await window.agentCompany.saveSecret({ companyId, name: name.trim(), description, value })); setName(""); setDescription(""); setValue(""); setErrors({}); await onSaved() } catch (e) { setSubmitError(describeActionError(e)) } finally { setSubmitting(false) } })()} icon={ShieldCheck} loading={submitting} />
|
||||
<div className="border-t border-border pt-4 text-[12px] leading-relaxed text-muted-foreground">Connector secret bindings use `ENV_NAME=secret-name` lines. Values stay outside renderer state after submission.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export function TaskCreator({ companyId, goals, projects, agents, workspaces, on
|
||||
<div className="grid min-w-0 gap-4 md:grid-cols-2">
|
||||
<Input label={t("exec.title")} value={title} onChange={setTitle} placeholder={t("exec.taskTitle")} />
|
||||
<Select label={t("exec.priority")} value={priority} onChange={setPriority} options={["critical","high","medium","low"].map((v) => ({ value: v, label: v }))} />
|
||||
<div className="md:col-span-2"><TextArea label="Description" value={description} onChange={setDescription} placeholder="Describe the task" rows={5} /></div>
|
||||
<div className="md:col-span-2"><TextArea label={t("exec.taskDescription")} value={description} onChange={setDescription} placeholder={t("exec.describeTask")} rows={5} /></div>
|
||||
<div className="md:col-span-2">
|
||||
<button type="button" onClick={() => setShowAdvanced(!showAdvanced)} className="flex items-center gap-1.5 text-[12px] font-medium text-muted-foreground transition hover:text-foreground">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}><path d="M3 1l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" /></svg>
|
||||
|
||||
@@ -38,7 +38,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ConfirmDialog open={confirmDelete} title={t("exec.deleteTask")} message={`${t("exec.deleteTaskConfirm").replace("{0}", task.title)}`} errorMessage={deleteError} onConfirm={() => void handleDelete()} onCancel={() => { setDeleteError(null); setConfirmDelete(false) }} />
|
||||
{budgetWarning > 0.8 && assignee ? <div className={`flex items-center gap-2 rounded-lg border px-4 py-3 text-[13px] font-medium ${budgetWarning > 1 ? "border-[color:var(--destructive)] bg-destructive-muted text-destructive" : "border-[color:var(--destructive)] bg-warning-muted text-warning"}`}><Warning size={16} weight="fill" />{assignee.name} has used {Math.round(budgetWarning * 100)}% of their monthly budget ({formatMoney(assignee.spentMonthlyUsd)} / {formatMoney(assignee.budgetMonthlyUsd)})</div> : null}
|
||||
{budgetWarning > 0.8 && assignee ? <div className={`flex items-center gap-2 rounded-lg border px-4 py-3 text-[13px] font-medium ${budgetWarning > 1 ? "border-[color:var(--destructive)] bg-destructive-muted text-destructive" : "border-[color:var(--destructive)] bg-warning-muted text-warning"}`}><Warning size={16} weight="fill" />{assignee.name} {t("exec.budgetUsed").replace("{0}", String(Math.round(budgetWarning * 100)))} ({formatMoney(assignee.spentMonthlyUsd)} / {formatMoney(assignee.budgetMonthlyUsd)})</div> : null}
|
||||
{actionError ? <InlineNotice message={actionError} /> : null}
|
||||
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
|
||||
{editing ? (
|
||||
@@ -56,15 +56,15 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-x-6 gap-y-2 text-[13px] md:grid-cols-2">
|
||||
{assignee && <div className="text-muted-foreground">Assignee: <span className="text-foreground">{assignee.name}</span></div>}
|
||||
{project && <div className="text-muted-foreground">Project: <span className="text-foreground">{project.name}</span></div>}
|
||||
{goal && <div className="text-muted-foreground">Goal: <span className="text-foreground">{goal.title}</span></div>}
|
||||
{task.parentId && (() => { const parent = tasks.find((t) => t.id === task.parentId); return parent ? <div className="text-muted-foreground">Parent: <span className="text-foreground">{parent.title}</span></div> : null })()}
|
||||
{workspace && <div className="text-muted-foreground">Workspace: <span className="text-foreground">{workspace.name}</span></div>}
|
||||
{task.costUsd > 0 && <div className="text-muted-foreground">Cost: <span className="font-mono tabular-nums text-foreground">{formatMoney(task.costUsd)}</span></div>}
|
||||
{assignee && <div className="text-muted-foreground">{t("exec.assigneeLabel")} <span className="text-foreground">{assignee.name}</span></div>}
|
||||
{project && <div className="text-muted-foreground">{t("exec.projectLabel")} <span className="text-foreground">{project.name}</span></div>}
|
||||
{goal && <div className="text-muted-foreground">{t("exec.goalLabel")} <span className="text-foreground">{goal.title}</span></div>}
|
||||
{task.parentId && (() => { const parent = tasks.find((t) => t.id === task.parentId); return parent ? <div className="text-muted-foreground">{t("exec.parentLabel")} <span className="text-foreground">{parent.title}</span></div> : null })()}
|
||||
{workspace && <div className="text-muted-foreground">{t("exec.workspaceLabel")} <span className="text-foreground">{workspace.name}</span></div>}
|
||||
{task.costUsd > 0 && <div className="text-muted-foreground">{t("exec.costLabel")} <span className="font-mono tabular-nums text-foreground">{formatMoney(task.costUsd)}</span></div>}
|
||||
</div>
|
||||
{task.description ? <div className="text-[13px] leading-relaxed text-muted-foreground whitespace-pre-wrap">{task.description}</div> : null}
|
||||
{task.status === "in_review" && reviewer ? <div className="rounded-lg border border-[color:var(--primary)] bg-accent px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-primary"><Robot size={16} weight="fill" />Being reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</div><div className="mt-1 text-[13px] leading-relaxed text-primary">{assignee?.name} submitted this task for review. {reviewer.name} will approve, request revisions, or escalate.</div></div>
|
||||
{task.status === "in_review" && reviewer ? <div className="rounded-lg border border-[color:var(--primary)] bg-accent px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-primary"><Robot size={16} weight="fill" />{t("exec.beingReviewedBy").replace("{0}", reviewer.name + (reviewer.title ? ` (${reviewer.title})` : ""))}</div><div className="mt-1 text-[13px] leading-relaxed text-primary">{t("exec.submittedForReview").replace("{0}", assignee?.name ?? "").replace("{1}", reviewer.name)}</div></div>
|
||||
: needsGovernance ? <div className="rounded-lg border border-[color:var(--destructive)] bg-warning-muted px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-warning"><ShieldCheck size={16} weight="fill" />{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}</div><div className="mt-1 text-[13px] leading-relaxed text-muted-foreground">{task.status === "blocked" ? t("exec.blockedGovernance") : t("exec.reviewGovernance")}</div><div className="mt-3"><ActionButton label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")} onClick={() => void handleCreateApproval()} icon={ShieldCheck} loading={creatingApproval} /></div></div> : null}
|
||||
<div className="flex items-center gap-3">
|
||||
{activeRun ? <ActionButton label={t("exec.cancelRun")} onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" /> : <>
|
||||
@@ -80,7 +80,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
)}
|
||||
{(() => { const subtasks = tasks.filter((t) => t.parentId === task.id); if (subtasks.length === 0) return null; return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2"><ListChecks size={14} className="text-muted-foreground" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-success">{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done</span></div>
|
||||
<div className="flex items-center gap-2"><ListChecks size={14} className="text-muted-foreground" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-success">{t("exec.doneCount").replace("{0}", String(subtasks.filter((st) => st.status === "done").length)).replace("{1}", String(subtasks.length))}</span></div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-success transition-all duration-300" style={{ width: `${subtasks.length > 0 ? (subtasks.filter((t) => t.status === "done").length / subtasks.length) * 100 : 0}%` }} /></div>
|
||||
<div className="space-y-1">{subtasks.map((sub) => { const subAssignee = agentMap.get(sub.assigneeAgentId ?? ""); return (
|
||||
<div key={sub.id} className="flex items-center gap-2 rounded-lg px-3 py-2 text-[12px] bg-muted">
|
||||
@@ -91,9 +91,9 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
<TaskComments taskId={task.id} companyId={task.companyId} agents={agents} comments={comments} onRefresh={onRefresh} />
|
||||
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agentMap.get(run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
|
||||
<div key={run.id} className={`rounded-lg border px-3 py-2.5 ${isActive ? "border-[color:var(--primary)] bg-success-muted" : run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--destructive)] border-opacity-30 bg-destructive-muted" : "border-border bg-muted"}`}>
|
||||
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-success" /> : null}<span className="truncate text-[13px] font-medium text-foreground">{run.summary || (isActive ? "Running..." : "Completed run")}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-muted-foreground">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="font-mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="font-mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
|
||||
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-success" /> : null}<span className="truncate text-[13px] font-medium text-foreground">{run.summary || (isActive ? t("exec.runningEllipsis") : t("exec.completedRun"))}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-muted-foreground">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="font-mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="font-mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
|
||||
{run.errorMessage ? <div className="mt-1.5 rounded bg-destructive-muted px-2 py-1 text-[11px] text-destructive break-words">{run.errorMessage}</div> : null}
|
||||
</div>) })}{runs.length > 8 && <div className="text-[11px] text-muted-foreground text-center">+{runs.length - 8} more runs</div>}</div></div>}
|
||||
</div>) })}{runs.length > 8 && <div className="text-[11px] text-muted-foreground text-center">{t("exec.moreRuns").replace("{0}", String(runs.length - 8))}</div>}</div></div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -200,8 +200,9 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
|
||||
const ag = candidate.agent;
|
||||
if (!ag) return null;
|
||||
const manager = agents.find(a => a.id === ag.reportsTo);
|
||||
const requestedBy = candidate.approval?.requestedByAgentId
|
||||
? agents.find(a => a.id === candidate.approval!.requestedByAgentId)
|
||||
const approvalRequestedBy = candidate.approval?.requestedByAgentId ?? null;
|
||||
const requestedBy = approvalRequestedBy
|
||||
? agents.find(a => a.id === approvalRequestedBy)
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/type
|
||||
import { isConnectorExecutionReady } from "@shared/connector-policy";
|
||||
import { unwrap } from "../../lib/desktop";
|
||||
import { ActionButton, Input, Select, TextArea } from "../ui";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
export function AgentCreator({
|
||||
companyId,
|
||||
@@ -66,27 +67,28 @@ export function AgentCreator({
|
||||
}
|
||||
}, [executionReadyConnectors, connectorId]);
|
||||
|
||||
const t = useT();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 gap-4">
|
||||
<Input label="Agent name" value={name} onChange={setName} placeholder="Agent name" hint="Display name shown in org chart and messages" />
|
||||
<Input label="Role" value={role} onChange={setRole} placeholder="e.g. Engineer, PM, CEO" hint="Determines which tasks the agent can handle" />
|
||||
<Input label="Title" value={title} onChange={setTitle} placeholder="Title" hint="Job title shown under the name" />
|
||||
<Input label={t("org.agentName")} value={name} onChange={setName} placeholder={t("org.agentName")} hint="Display name shown in org chart and messages" />
|
||||
<Input label={t("agent.role")} value={role} onChange={setRole} placeholder="e.g. Engineer, PM, CEO" hint="Determines which tasks the agent can handle" />
|
||||
<Input label={t("agent.title")} value={title} onChange={setTitle} placeholder={t("agent.title")} hint="Job title shown under the name" />
|
||||
<Select
|
||||
label="Connector"
|
||||
label={t("org.connector")}
|
||||
value={connectorId}
|
||||
onChange={(value) => setConnectorId(value as ConnectorRecord["id"])}
|
||||
options={executionReadyConnectors.map((connector) => ({ value: connector.id, label: connector.id.startsWith("claude_compat") && connector.notes ? connector.notes : connector.label }))}
|
||||
/>
|
||||
{executionReadyConnectors.length === 0 ? (
|
||||
<p className="text-[12px] text-warning">
|
||||
No execution-ready connector is available. Fix connector health before requesting hires.
|
||||
{t("org.noConnectorAvailable")}
|
||||
</p>
|
||||
) : null}
|
||||
{availableModels.length > 0 ? (
|
||||
<Select
|
||||
label="Model"
|
||||
label={t("org.model")}
|
||||
value={model}
|
||||
onChange={setModel}
|
||||
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
|
||||
@@ -94,11 +96,11 @@ export function AgentCreator({
|
||||
) : null}
|
||||
{supportsEffort ? (
|
||||
<Select
|
||||
label="Thinking effort"
|
||||
label={t("org.thinkingEffort")}
|
||||
value={thinkingEffort}
|
||||
onChange={setThinkingEffort}
|
||||
options={[
|
||||
{ value: "", label: "Default" },
|
||||
{ value: "", label: t("org.default") },
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
@@ -107,32 +109,32 @@ export function AgentCreator({
|
||||
) : null}
|
||||
<button type="button" onClick={() => setShowAdvanced(!showAdvanced)} className="flex items-center gap-1.5 text-[12px] font-medium text-muted-foreground transition hover:text-foreground">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}><path d="M3 1l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" /></svg>
|
||||
Advanced settings
|
||||
{t("org.advancedSettings")}
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{showAdvanced ? (
|
||||
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }} className="grid gap-4 overflow-hidden">
|
||||
<Select
|
||||
label="Workspace"
|
||||
label={t("agent.workspace")}
|
||||
value={workspaceId}
|
||||
onChange={setWorkspaceId}
|
||||
options={[
|
||||
{ value: "", label: "No workspace" },
|
||||
{ value: "", label: t("agent.noWorkspace") },
|
||||
...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name })),
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Reports to"
|
||||
label={t("agent.reportsTo")}
|
||||
value={reportsTo}
|
||||
onChange={setReportsTo}
|
||||
options={[
|
||||
{ value: "", label: "No manager" },
|
||||
{ value: "", label: t("agent.noManager") },
|
||||
...agents.map((a) => ({ value: a.id, label: a.name })),
|
||||
]}
|
||||
/>
|
||||
<Input label="Monthly budget (USD)" value={budget} onChange={setBudget} placeholder="120" type="number" hint="Spending cap per month; agent pauses when exceeded" />
|
||||
<Input label={t("org.monthlyBudget")} value={budget} onChange={setBudget} placeholder="120" type="number" hint="Spending cap per month; agent pauses when exceeded" />
|
||||
<TextArea
|
||||
label="Capabilities"
|
||||
label={t("agent.capabilities")}
|
||||
value={capabilities}
|
||||
onChange={setCapabilities}
|
||||
placeholder="What this agent does"
|
||||
@@ -142,17 +144,17 @@ export function AgentCreator({
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
New agents enter the hiring pipeline first. Approval activates the agent and keeps onboarding, budgets, and org history linked to the same record.
|
||||
{t("org.hireRequestNote")}
|
||||
</p>
|
||||
<ActionButton
|
||||
label="Submit hire request"
|
||||
label={t("org.submitHireRequest")}
|
||||
tone="accent"
|
||||
onClick={() =>
|
||||
void (async () => {
|
||||
try {
|
||||
setRequestError(null);
|
||||
if (!selectedConnector) {
|
||||
setRequestError("Select an execution-ready connector before requesting a hire.");
|
||||
setRequestError(t("org.selectConnectorFirst"));
|
||||
return;
|
||||
}
|
||||
const meta: Record<string, string> = {};
|
||||
@@ -180,7 +182,7 @@ export function AgentCreator({
|
||||
setRequestError(null);
|
||||
await onSaved();
|
||||
} catch (error) {
|
||||
setRequestError(error instanceof Error ? error.message : "Hire request failed.");
|
||||
setRequestError(error instanceof Error ? error.message : t("org.hireRequestFailed"));
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -89,15 +89,15 @@ export function AgentRoster({
|
||||
<div>
|
||||
<div className={`truncate text-[14px] font-semibold text-foreground`}>{agent.name}</div>
|
||||
<div className="truncate text-[11px] text-muted-foreground">
|
||||
{agent.role} · {agent.title || "No title"}
|
||||
{agent.role} · {agent.title || t("org.noTitle")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<StatusPill status={agent.status} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span>Connector: {connector?.id.startsWith("claude_compat") && connector.notes ? connector.notes : connector?.label ?? agent.connectorId}</span>
|
||||
{workspace && <span>Workspace: {workspace.name}</span>}
|
||||
<span>{t("org.connectorLabel")} {connector?.id.startsWith("claude_compat") && connector.notes ? connector.notes : connector?.label ?? agent.connectorId}</span>
|
||||
{workspace && <span>{t("org.workspaceLabel")} {workspace.name}</span>}
|
||||
</div>
|
||||
<div className="mt-1.5">
|
||||
<BudgetBar spent={agent.spentMonthlyUsd} budget={agent.budgetMonthlyUsd} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { Target } from "@phosphor-icons/react";
|
||||
import type { AgentRecord, GoalRecord } from "@shared/types";
|
||||
import { ActionButton, Input, Select, TextArea } from "../ui";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
export function GoalCreator({
|
||||
companyId,
|
||||
@@ -14,39 +15,40 @@ export function GoalCreator({
|
||||
goals: GoalRecord[];
|
||||
onSaved: () => Promise<void>;
|
||||
}) {
|
||||
const t = useT();
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [ownerAgentId, setOwnerAgentId] = useState(agents[0]?.id ?? "");
|
||||
const [parentId, setParentId] = useState("");
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<Input label="Goal title" value={title} onChange={setTitle} placeholder="Goal title" />
|
||||
<Input label={t("org.goalTitle")} value={title} onChange={setTitle} placeholder={t("org.goalTitle")} />
|
||||
<TextArea
|
||||
label="Success criteria"
|
||||
label={t("org.successCriteria")}
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
placeholder="Define the acceptance criteria"
|
||||
placeholder={t("org.successCriteria")}
|
||||
/>
|
||||
<Select
|
||||
label="Owner agent"
|
||||
label={t("org.ownerAgent")}
|
||||
value={ownerAgentId}
|
||||
onChange={setOwnerAgentId}
|
||||
options={[
|
||||
{ value: "", label: "No owner" },
|
||||
{ value: "", label: t("org.noOwner") },
|
||||
...agents.map((agent) => ({ value: agent.id, label: agent.name })),
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Parent goal"
|
||||
label={t("org.parentGoal")}
|
||||
value={parentId}
|
||||
onChange={setParentId}
|
||||
options={[
|
||||
{ value: "", label: "No parent goal" },
|
||||
{ value: "", label: t("org.noParentGoal") },
|
||||
...goals.map((g) => ({ value: g.id, label: g.title })),
|
||||
]}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Create goal"
|
||||
label={t("org.createGoal")}
|
||||
tone="accent"
|
||||
onClick={() =>
|
||||
void (async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Pencil, Trash } from "@phosphor-icons/react";
|
||||
import type { AgentRecord, GoalRecord } from "@shared/types";
|
||||
import { ActionButton, Input, Select, TextArea } from "../ui";
|
||||
import { ConfirmDelete } from "./helpers";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
export function GoalEditor({
|
||||
goal,
|
||||
@@ -24,47 +25,48 @@ export function GoalEditor({
|
||||
const [status, setStatus] = useState<GoalRecord["status"]>(goal.status);
|
||||
const [parentId, setParentId] = useState(goal.parentId ?? "");
|
||||
const [ownerAgentId, setOwnerAgentId] = useState(goal.ownerAgentId ?? "");
|
||||
const t = useT();
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||
|
||||
const parentCandidates = goals.filter((g) => g.id !== goal.id);
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<Input label="Goal title" value={title} onChange={setTitle} placeholder="Goal title" />
|
||||
<TextArea label="Description" value={description} onChange={setDescription} placeholder="Description" />
|
||||
<Input label={t("org.goalTitle")} value={title} onChange={setTitle} placeholder={t("org.goalTitle")} />
|
||||
<TextArea label={t("org.description")} value={description} onChange={setDescription} placeholder={t("org.description")} />
|
||||
<Select
|
||||
label="Status"
|
||||
label={t("org.status")}
|
||||
value={status}
|
||||
onChange={(v) => setStatus(v as GoalRecord["status"])}
|
||||
options={[
|
||||
{ value: "planned", label: "Planned" },
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "achieved", label: "Achieved" },
|
||||
{ value: "cancelled", label: "Cancelled" },
|
||||
{ value: "planned", label: t("org.statusPlanned") },
|
||||
{ value: "active", label: t("org.statusActive") },
|
||||
{ value: "achieved", label: t("org.statusAchieved") },
|
||||
{ value: "cancelled", label: t("org.statusCancelled") },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Parent goal"
|
||||
label={t("org.parentGoal")}
|
||||
value={parentId}
|
||||
onChange={setParentId}
|
||||
options={[
|
||||
{ value: "", label: "No parent goal" },
|
||||
{ value: "", label: t("org.noParentGoal") },
|
||||
...parentCandidates.map((g) => ({ value: g.id, label: g.title })),
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Owner agent"
|
||||
label={t("org.ownerAgent")}
|
||||
value={ownerAgentId}
|
||||
onChange={setOwnerAgentId}
|
||||
options={[
|
||||
{ value: "", label: "No owner" },
|
||||
{ value: "", label: t("org.noOwner") },
|
||||
...agents.map((a) => ({ value: a.id, label: a.name })),
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<ActionButton
|
||||
label="Save changes"
|
||||
label={t("org.saveChanges")}
|
||||
tone="accent"
|
||||
icon={Pencil}
|
||||
onClick={() =>
|
||||
@@ -99,7 +101,7 @@ export function GoalEditor({
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
label="Delete"
|
||||
label={t("common.delete")}
|
||||
tone="danger"
|
||||
icon={Trash}
|
||||
onClick={() => setConfirmingDelete(true)}
|
||||
|
||||
@@ -45,15 +45,15 @@ export function ProjectList({
|
||||
<div className="mb-2 text-[13px] text-muted-foreground">{project.description}</div>
|
||||
<div className="flex flex-wrap gap-4 text-[11px] text-muted-foreground">
|
||||
<span>
|
||||
Goal: {goals.find((g) => g.id === project.goalId)?.title ?? "None"}
|
||||
{t("org.goalLabel")} {goals.find((g) => g.id === project.goalId)?.title ?? t("exec.noGoal")}
|
||||
</span>
|
||||
<span>
|
||||
Lead: {agents.find((a) => a.id === project.leadAgentId)?.name ?? "Unassigned"}
|
||||
{t("org.leadLabel")} {agents.find((a) => a.id === project.leadAgentId)?.name ?? t("exec.unassigned")}
|
||||
</span>
|
||||
{project.targetDate && <span>Target: {project.targetDate}</span>}
|
||||
{project.targetDate && <span>{t("org.targetLabel")} {project.targetDate}</span>}
|
||||
{wsCount > 0 && (
|
||||
<span>
|
||||
{wsCount} workspace{wsCount !== 1 ? "s" : ""}
|
||||
{wsCount !== 1 ? t("org.workspacesCount").replace("{0}", String(wsCount)) : t("org.workspaceCount").replace("{0}", String(wsCount))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@ export function RunDetail({
|
||||
<ActionButton
|
||||
label={t("runs.openLogFile")}
|
||||
icon={FolderOpen}
|
||||
onClick={() => void onOpenLogPath(selectedRun.logPath!)}
|
||||
onClick={() => { const p = selectedRun.logPath; if (p) void onOpenLogPath(p); }}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
|
||||
{action.taskId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate("tasks", action.taskId!)}
|
||||
onClick={() => { const id = action.taskId; if (id) onNavigate("tasks", id); }}
|
||||
className="rounded-[5px] bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
Open task
|
||||
@@ -74,7 +74,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
|
||||
{action.approvalId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate("approvals", action.approvalId!)}
|
||||
onClick={() => { const id = action.approvalId; if (id) onNavigate("approvals", id); }}
|
||||
className="rounded-[5px] bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
Open approval
|
||||
@@ -83,7 +83,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
|
||||
{action.screenshotPath ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onOpenPath(action.screenshotPath!)}
|
||||
onClick={() => { const p = action.screenshotPath; if (p) void onOpenPath(p); }}
|
||||
className="inline-flex items-center gap-1 rounded-[5px] bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
<ArrowSquareOut size={10} />
|
||||
|
||||
@@ -57,7 +57,7 @@ export function SocialAccountsPanel({
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton
|
||||
label="Add Account"
|
||||
label={t("social.addAccount")}
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
icon={Plus}
|
||||
tone="accent"
|
||||
@@ -68,11 +68,9 @@ export function SocialAccountsPanel({
|
||||
<div className="flex items-start gap-2 text-[12px] text-primary">
|
||||
<Browser size={16} className="mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium">How it works</div>
|
||||
<div className="font-medium">{t("social.howItWorks")}</div>
|
||||
<div className="mt-1 text-[11px] opacity-80">
|
||||
Add your social media accounts here. Click "Login" to open a browser where you can sign in.
|
||||
Your session is saved locally so marketing agents can post content, reply to users, and browse feeds
|
||||
using browser automation. All actions are logged and can require Board approval.
|
||||
{t("social.howItWorksDesc")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +86,7 @@ export function SocialAccountsPanel({
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Accounts ({accounts.length})
|
||||
{t("social.accounts")} ({accounts.length})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -99,7 +97,7 @@ export function SocialAccountsPanel({
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Action History ({actions.length})
|
||||
{t("social.actionHistory")} ({actions.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +111,7 @@ export function SocialAccountsPanel({
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<ActionButton
|
||||
label="Add Account"
|
||||
label={t("social.addAccount")}
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
icon={Plus}
|
||||
tone="accent"
|
||||
|
||||
@@ -1,389 +1,209 @@
|
||||
import { useCallback, useDeferredValue, useEffect, useMemo, useRef } from "react";
|
||||
import type {
|
||||
CompanyRecord,
|
||||
OnboardingBootstrapResult,
|
||||
SectionId,
|
||||
} from "@shared/types";
|
||||
import type { AppLocale } from "@shared/locale";
|
||||
import { createTranslator } from "../i18n";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useAppStore } from "../state/store";
|
||||
import { unwrap } from "../lib/desktop";
|
||||
import { useFocusTrap, useReducedMotion } from "../components/ui";
|
||||
import {
|
||||
BookOpen,
|
||||
Gear,
|
||||
Tray,
|
||||
SquaresFour,
|
||||
UsersThree,
|
||||
ListChecks,
|
||||
ChatCircle,
|
||||
PuzzlePiece,
|
||||
FileText,
|
||||
ShieldCheck,
|
||||
Megaphone,
|
||||
FolderOpen,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useDesktopEvents } from "./useDesktopEvents";
|
||||
import { useDataLoader } from "./useDataLoader";
|
||||
import { useCompanyData } from "./useCompanyData";
|
||||
import { useUIController } from "./useUIController";
|
||||
import { useCompanyController } from "./useCompanyController";
|
||||
import { useExecutionController } from "./useExecutionController";
|
||||
import { useOrganizationController } from "./useOrganizationController";
|
||||
import { useCommunicationController } from "./useCommunicationController";
|
||||
import { useCommandPaletteHotkeys } from "./useCommandPaletteHotkeys";
|
||||
import { unwrap } from "../lib/desktop";
|
||||
|
||||
type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour };
|
||||
export type NavItem = { id: SectionId; label: string; icon: typeof SquaresFour };
|
||||
export type PaletteAction = { key: string; section: SectionId; entityId?: string };
|
||||
|
||||
const sidebarNavDefs: NavItemDef[] = [
|
||||
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
|
||||
{ id: "inbox", labelKey: "nav.inbox", icon: Tray },
|
||||
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
|
||||
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
|
||||
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
|
||||
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
|
||||
{ id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen },
|
||||
{ id: "social", labelKey: "nav.social", icon: Megaphone },
|
||||
{ id: "documents", labelKey: "nav.documents", icon: FileText },
|
||||
];
|
||||
|
||||
const paletteOnlyNavDefs: NavItemDef[] = [
|
||||
{ id: "skills", labelKey: "nav.skills", icon: PuzzlePiece },
|
||||
{ id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen },
|
||||
];
|
||||
// Re-export types consumed by other modules
|
||||
export type { NavItem, PaletteAction } from "./useUIController";
|
||||
|
||||
export function useAppController() {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const noMotion = { duration: 0 };
|
||||
const pageTransition = reducedMotion ? noMotion : { duration: 0.32, ease: [0.16, 1, 0.3, 1] as const };
|
||||
const panelTransition = reducedMotion ? noMotion : { duration: 0.28, ease: [0.16, 1, 0.3, 1] as const };
|
||||
const ui = useUIController();
|
||||
const company = useCompanyController();
|
||||
const execution = useExecutionController();
|
||||
const organization = useOrganizationController();
|
||||
const communication = useCommunicationController();
|
||||
|
||||
const locale = useAppStore((s) => s.locale);
|
||||
const setLocale = useAppStore((s) => s.setLocale);
|
||||
const t = useMemo(() => createTranslator(locale), [locale]);
|
||||
|
||||
const sidebarNav: NavItem[] = useMemo(() => sidebarNavDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })), [t]);
|
||||
const navItems: NavItem[] = useMemo(
|
||||
() =>
|
||||
sidebarNav
|
||||
.concat(
|
||||
paletteOnlyNavDefs.map((item) => ({
|
||||
id: item.id,
|
||||
label: t(item.labelKey),
|
||||
icon: item.icon,
|
||||
})),
|
||||
)
|
||||
.concat({ id: "settings", label: t("nav.settings"), icon: Gear }),
|
||||
[sidebarNav, t],
|
||||
);
|
||||
|
||||
const snapshot = useAppStore((s) => s.snapshot);
|
||||
const setSnapshot = useAppStore((s) => s.setSnapshot);
|
||||
const section = useAppStore((s) => s.section);
|
||||
const setSection = useAppStore((s) => s.setSection);
|
||||
const error = useAppStore((s) => s.error);
|
||||
const setError = useAppStore((s) => s.setError);
|
||||
const commandPaletteOpen = useAppStore((s) => s.commandPaletteOpen);
|
||||
const setCommandPaletteOpen = useAppStore((s) => s.setCommandPaletteOpen);
|
||||
const consoleOpen = useAppStore((s) => s.consoleOpen);
|
||||
const setConsoleOpen = useAppStore((s) => s.setConsoleOpen);
|
||||
const sidebarCollapsed = useAppStore((s) => s.sidebarCollapsed);
|
||||
const setSidebarCollapsed = useAppStore((s) => s.setSidebarCollapsed);
|
||||
const companySwitcherOpen = useAppStore((s) => s.companySwitcherOpen);
|
||||
const setCompanySwitcherOpen = useAppStore((s) => s.setCompanySwitcherOpen);
|
||||
const confirmDeleteCompanyId = useAppStore((s) => s.confirmDeleteCompanyId);
|
||||
const setConfirmDeleteCompanyId = useAppStore((s) => s.setConfirmDeleteCompanyId);
|
||||
const selectedTaskId = useAppStore((s) => s.selectedTaskId);
|
||||
const setSelectedTaskId = useAppStore((s) => s.setSelectedTaskId);
|
||||
const selectedConnectorId = useAppStore((s) => s.selectedConnectorId);
|
||||
const setSelectedConnectorId = useAppStore((s) => s.setSelectedConnectorId);
|
||||
const selectedApprovalId = useAppStore((s) => s.selectedApprovalId);
|
||||
const setSelectedApprovalId = useAppStore((s) => s.setSelectedApprovalId);
|
||||
const selectedRunId = useAppStore((s) => s.selectedRunId);
|
||||
const setSelectedRunId = useAppStore((s) => s.setSelectedRunId);
|
||||
const selectedAgentId = useAppStore((s) => s.selectedAgentId);
|
||||
const setSelectedAgentId = useAppStore((s) => s.setSelectedAgentId);
|
||||
const selectedGoalId = useAppStore((s) => s.selectedGoalId);
|
||||
const setSelectedGoalId = useAppStore((s) => s.setSelectedGoalId);
|
||||
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||
const setSelectedWorkspaceId = useAppStore((s) => s.setSelectedWorkspaceId);
|
||||
const selectedCommunicationEntityId = useAppStore((s) => s.selectedCommunicationEntityId);
|
||||
const setSelectedCommunicationEntityId = useAppStore((s) => s.setSelectedCommunicationEntityId);
|
||||
const taskViewMode = useAppStore((s) => s.taskViewMode);
|
||||
const setTaskViewMode = useAppStore((s) => s.setTaskViewMode);
|
||||
const logBufferRef = useRef<Record<string, string>>({});
|
||||
const incrementLogRevision = useAppStore((s) => s.incrementLogRevision);
|
||||
const commandPaletteSearch = useAppStore((s) => s.commandPaletteSearch);
|
||||
const setCommandPaletteSearch = useAppStore((s) => s.setCommandPaletteSearch);
|
||||
const selectedPaletteIndex = useAppStore((s) => s.selectedPaletteIndex);
|
||||
const setSelectedPaletteIndex = useAppStore((s) => s.setSelectedPaletteIndex);
|
||||
const showOnboarding = useAppStore((s) => s.showOnboarding);
|
||||
const setShowOnboarding = useAppStore((s) => s.setShowOnboarding);
|
||||
const initialOnboardingLocked = useAppStore((s) => s.initialOnboardingLocked);
|
||||
const setInitialOnboardingLocked = useAppStore((s) => s.setInitialOnboardingLocked);
|
||||
const inboxItems = useAppStore((s) => s.inboxItems);
|
||||
const setInboxItems = useAppStore((s) => s.setInboxItems);
|
||||
const companyMetrics = useAppStore((s) => s.companyMetrics);
|
||||
const setCompanyMetrics = useAppStore((s) => s.setCompanyMetrics);
|
||||
const standupReport = useAppStore((s) => s.standupReport);
|
||||
const setStandupReport = useAppStore((s) => s.setStandupReport);
|
||||
const socialAccounts = useAppStore((s) => s.socialAccounts);
|
||||
const setSocialAccounts = useAppStore((s) => s.setSocialAccounts);
|
||||
const browserActions = useAppStore((s) => s.browserActions);
|
||||
const setBrowserActions = useAppStore((s) => s.setBrowserActions);
|
||||
const setBadges = useAppStore((s) => s.setBadges);
|
||||
|
||||
const currentCompany = useMemo<CompanyRecord | null>(() => {
|
||||
if (!snapshot) return null;
|
||||
return snapshot.companies.find((company) => company.id === snapshot.currentCompanyId) ?? snapshot.companies[0] ?? null;
|
||||
}, [snapshot]);
|
||||
|
||||
const companyData = useCompanyData(snapshot, currentCompany);
|
||||
const { tasks, runs, approvals, pendingApprovals, companyAgents, companyGoals, companyProjects,
|
||||
companyWorkspaces, companyCosts, filteredActivity, companySecrets, companyDocuments,
|
||||
companyKnowledge, companyMeetings, companySprints, selectedTask, selectedApproval,
|
||||
selectedRun, selectedAgent, selectedGoal, selectedProject, selectedWorkspace, navBadges } = companyData;
|
||||
|
||||
const selectedConnector = snapshot?.connectors.find((connector) => connector.id === selectedConnectorId) ?? snapshot?.connectors[0] ?? null;
|
||||
|
||||
const clearCompanyScopedUiState = useCallback(() => {
|
||||
const store = useAppStore.getState();
|
||||
store.clearCompanyScopedSelections();
|
||||
store.setInboxItems([]);
|
||||
store.setCompanyMetrics(null);
|
||||
store.setStandupReport(null);
|
||||
store.setSocialAccounts([]);
|
||||
store.setBrowserActions([]);
|
||||
store.setBadges(null);
|
||||
store.setSkills([]);
|
||||
store.setSelectedSkillId(null);
|
||||
store.setSelectedSkillFilePath("SKILL.md");
|
||||
store.setSkillImportModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const { refresh } = useDataLoader(
|
||||
snapshot, section, selectedConnectorId, selectedRunId,
|
||||
setSnapshot, setError, setInitialOnboardingLocked,
|
||||
setSelectedConnectorId, setSelectedRunId,
|
||||
setBadges, setInboxItems, setSocialAccounts, setBrowserActions,
|
||||
setCompanyMetrics, setStandupReport,
|
||||
);
|
||||
|
||||
// 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]);
|
||||
|
||||
useDesktopEvents(refreshRef, logBufferRef, incrementLogRevision);
|
||||
|
||||
useEffect(() => {
|
||||
if (!snapshot) return;
|
||||
const root = document.documentElement;
|
||||
const applyTheme = () => {
|
||||
const isDark = snapshot.theme === "dark" ||
|
||||
(snapshot.theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
root.classList.toggle("dark", isDark);
|
||||
root.style.colorScheme = isDark ? "dark" : "light";
|
||||
};
|
||||
applyTheme();
|
||||
if (snapshot.locale && snapshot.locale !== locale) setLocale(snapshot.locale);
|
||||
if (snapshot.theme === "system") {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mq.addEventListener("change", applyTheme);
|
||||
return () => mq.removeEventListener("change", applyTheme);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setLocale is a stable Zustand setter
|
||||
}, [snapshot, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale === "zh" ? "zh-CN" : "en";
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRunId && !runs.some((run) => run.id === selectedRunId)) {
|
||||
setSelectedRunId(runs[0]?.id ?? null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectedRunId is a stable Zustand setter
|
||||
}, [runs, selectedRunId]);
|
||||
|
||||
const activeRunId = selectedRun?.id ?? null;
|
||||
useEffect(() => {
|
||||
if (!activeRunId) return;
|
||||
void (async () => {
|
||||
const activeRun = runs.find((run) => run.id === activeRunId);
|
||||
if (!activeRun) return;
|
||||
const result = await window.agentCompany.getRunLog({ companyId: activeRun.companyId, runId: activeRunId });
|
||||
if (result.ok) {
|
||||
logBufferRef.current = { ...logBufferRef.current, [activeRunId]: result.data };
|
||||
incrementLogRevision();
|
||||
return;
|
||||
}
|
||||
setError(result.error.message);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- incrementLogRevision and setError are stable Zustand setters
|
||||
}, [activeRunId, runs]);
|
||||
|
||||
|
||||
const handleLocaleChange = useCallback(async (newLocale: AppLocale) => {
|
||||
setLocale(newLocale);
|
||||
try { await window.agentCompany.updateSettings({ locale: newLocale }); } catch { /* local state already updated */ }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setLocale is a stable Zustand setter
|
||||
}, []);
|
||||
|
||||
const paletteTrapRef = useFocusTrap(commandPaletteOpen);
|
||||
|
||||
const paletteQuery = useDeferredValue(commandPaletteSearch.trim().toLowerCase());
|
||||
const paletteResults = useMemo(() => {
|
||||
const filteredNavItems = paletteQuery
|
||||
? navItems.filter((item) => item.label.toLowerCase().includes(paletteQuery) || item.id.toLowerCase().includes(paletteQuery))
|
||||
: navItems;
|
||||
const matchedAgents = paletteQuery ? companyAgents.filter((a) => a.name.toLowerCase().includes(paletteQuery) || a.role.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const matchedTasks = paletteQuery ? tasks.filter((t) => t.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const matchedRuns = paletteQuery ? runs.filter((run) => {
|
||||
const matchingTask = tasks.find((task) => task.id === run.taskId);
|
||||
return [run.id, run.summary ?? "", run.errorMessage ?? "", matchingTask?.title ?? "", run.connectorId]
|
||||
.some((value) => value.toLowerCase().includes(paletteQuery));
|
||||
}).slice(0, 5) : [];
|
||||
const matchedGoals = paletteQuery ? companyGoals.filter((g) => g.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const matchedProjects = paletteQuery ? companyProjects.filter((p) => p.name.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const hasEntityResults = matchedAgents.length > 0 || matchedTasks.length > 0 || matchedRuns.length > 0 || matchedGoals.length > 0 || matchedProjects.length > 0;
|
||||
return { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults };
|
||||
}, [paletteQuery, companyAgents, companyGoals, companyProjects, runs, tasks, navItems]);
|
||||
const { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults } = paletteResults;
|
||||
|
||||
const paletteActions = useMemo<PaletteAction[]>(() => ([
|
||||
...filteredNavItems.map((item) => ({ key: `section:${item.id}`, section: item.id })),
|
||||
...matchedAgents.map((agent) => ({ key: `agent:${agent.id}`, section: "agents" as const, entityId: agent.id })),
|
||||
...matchedTasks.map((task) => ({ key: `task:${task.id}`, section: "tasks" as const, entityId: task.id })),
|
||||
...matchedRuns.map((run) => ({ key: `run:${run.id}`, section: "runs" as const, entityId: run.id })),
|
||||
...matchedGoals.map((goal) => ({ key: `goal:${goal.id}`, section: "goals" as const, entityId: goal.id })),
|
||||
...matchedProjects.map((project) => ({ key: `project:${project.id}`, section: "projects" as const, entityId: project.id })),
|
||||
]), [filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects]);
|
||||
const paletteActionIndex = useMemo(() => new Map(paletteActions.map((action, index) => [action.key, index])), [paletteActions]);
|
||||
|
||||
const navigateTo = useCallback((targetSection: SectionId, entityId?: string) => {
|
||||
setSection(targetSection);
|
||||
if (entityId) {
|
||||
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||
else if (targetSection === "connectors") setSelectedConnectorId(entityId);
|
||||
else if (targetSection === "runs") { setSelectedRunId(entityId); setConsoleOpen(true); }
|
||||
else if (targetSection === "tasks") {
|
||||
const matchingTask = tasks.find((task) => task.id === entityId) ?? null;
|
||||
if (matchingTask) { setSelectedTaskId(matchingTask.id); }
|
||||
else {
|
||||
const matchingRun = runs.find((run) => run.id === entityId) ?? null;
|
||||
if (matchingRun) { setSelectedRunId(matchingRun.id); setSelectedTaskId(matchingRun.taskId); setConsoleOpen(true); }
|
||||
}
|
||||
}
|
||||
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||
else if (targetSection === "communication") setSelectedCommunicationEntityId(entityId);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
|
||||
}, [runs, tasks]);
|
||||
|
||||
useCommandPaletteHotkeys({
|
||||
commandPaletteOpen,
|
||||
setCommandPaletteOpen,
|
||||
setCommandPaletteSearch,
|
||||
setSelectedPaletteIndex,
|
||||
paletteActions,
|
||||
selectedPaletteIndex,
|
||||
navigateTo,
|
||||
commandPaletteSearch,
|
||||
});
|
||||
|
||||
const setCompany = useCallback(async (companyId: string) => {
|
||||
if (companyId === "__new__") { setShowOnboarding(true); return; }
|
||||
try {
|
||||
unwrap(await window.agentCompany.setCurrentCompany(companyId));
|
||||
clearCompanyScopedUiState();
|
||||
await refresh();
|
||||
setError(null);
|
||||
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError and setShowOnboarding are stable Zustand setters
|
||||
}, [clearCompanyScopedUiState, refresh]);
|
||||
|
||||
const deleteCompany = useCallback(async (companyId: string) => {
|
||||
try {
|
||||
unwrap(await window.agentCompany.deleteCompany(companyId));
|
||||
setConfirmDeleteCompanyId(null);
|
||||
setCompanySwitcherOpen(false);
|
||||
await refresh();
|
||||
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
|
||||
}, [refresh]);
|
||||
// --- Cross-domain actions composed here ---
|
||||
|
||||
const startRun = useCallback(async (taskId: string) => {
|
||||
if (!currentCompany) { setError("Select a company before starting a run."); return; }
|
||||
const result = await window.agentCompany.startTaskRun({ companyId: currentCompany.id, taskId });
|
||||
if (!company.currentCompany) { setError("Select a company before starting a run."); return; }
|
||||
const result = await window.agentCompany.startTaskRun({ companyId: company.currentCompany.id, taskId });
|
||||
if (!result.ok) { setError(result.error.message); return; }
|
||||
setSelectedRunId(result.data);
|
||||
setConsoleOpen(true);
|
||||
await refresh();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
|
||||
}, [currentCompany, refresh]);
|
||||
execution.setSelectedRunId(result.data);
|
||||
ui.setConsoleOpen(true);
|
||||
await company.refresh();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters and company.refresh are stable
|
||||
}, [company.currentCompany, company.refresh]);
|
||||
|
||||
const cancelRun = useCallback(async (runId: string) => {
|
||||
try {
|
||||
const run = runs.find((entry) => entry.id === runId);
|
||||
const run = company.runs.find((entry) => entry.id === runId);
|
||||
if (!run) throw new Error("Selected run is no longer available.");
|
||||
unwrap(await window.agentCompany.cancelRun({ companyId: run.companyId, runId }));
|
||||
await refresh();
|
||||
await company.refresh();
|
||||
setError(null);
|
||||
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError is a stable Zustand setter
|
||||
}, [refresh, runs]);
|
||||
}, [company.refresh, company.runs]);
|
||||
|
||||
const handleOnboardingDone = useCallback(async (summary: OnboardingBootstrapResult) => {
|
||||
setShowOnboarding(false);
|
||||
setInitialOnboardingLocked(false);
|
||||
await refresh();
|
||||
setError(null);
|
||||
setSection("overview");
|
||||
if (summary.runId) setSelectedRunId(summary.runId);
|
||||
if (summary.leadAgentId) setSelectedAgentId(summary.leadAgentId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
|
||||
}, [refresh]);
|
||||
// --- Command palette results (needs entity lists from company domain) ---
|
||||
|
||||
const openPathWithFeedback = useCallback(async (path: string) => {
|
||||
try { unwrap(await window.agentCompany.openPath(path)); setError(null); }
|
||||
catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError is a stable Zustand setter
|
||||
}, []);
|
||||
const { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults } =
|
||||
useMemo(() => ui.buildPaletteResults(
|
||||
ui.navItems,
|
||||
company.companyAgents,
|
||||
company.tasks,
|
||||
company.runs,
|
||||
company.companyGoals,
|
||||
company.companyProjects,
|
||||
), [
|
||||
ui.navItems,
|
||||
ui.paletteQuery,
|
||||
company.companyAgents,
|
||||
company.tasks,
|
||||
company.runs,
|
||||
company.companyGoals,
|
||||
company.companyProjects,
|
||||
]);
|
||||
|
||||
const paletteActions = useMemo(
|
||||
() => ui.buildPaletteActions(filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects),
|
||||
[filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects],
|
||||
);
|
||||
|
||||
const paletteActionIndex = useMemo(
|
||||
() => new Map(paletteActions.map((action, index) => [action.key, index])),
|
||||
[paletteActions],
|
||||
);
|
||||
|
||||
// navigateTo needs entity lists for task/run cross-lookup
|
||||
const navigateTo = useCallback((targetSection: Parameters<typeof ui.navigateTo>[0], entityId?: string) => {
|
||||
ui.navigateTo(targetSection, entityId, company.runs, company.tasks);
|
||||
}, [ui.navigateTo, company.runs, company.tasks]);
|
||||
|
||||
// Wire command palette hotkeys
|
||||
useCommandPaletteHotkeys({
|
||||
commandPaletteOpen: ui.commandPaletteOpen,
|
||||
setCommandPaletteOpen: ui.setCommandPaletteOpen,
|
||||
setCommandPaletteSearch: ui.setCommandPaletteSearch,
|
||||
setSelectedPaletteIndex: ui.setSelectedPaletteIndex,
|
||||
paletteActions,
|
||||
selectedPaletteIndex: ui.selectedPaletteIndex,
|
||||
navigateTo,
|
||||
commandPaletteSearch: ui.commandPaletteSearch,
|
||||
});
|
||||
|
||||
return {
|
||||
pageTransition, panelTransition,
|
||||
locale, t, handleLocaleChange,
|
||||
sidebarNav, navItems, navBadges,
|
||||
snapshot, currentCompany,
|
||||
showOnboarding, initialOnboardingLocked, handleOnboardingDone, setShowOnboarding, setInitialOnboardingLocked, setError,
|
||||
sidebarCollapsed, setSidebarCollapsed,
|
||||
companySwitcherOpen, setCompanySwitcherOpen,
|
||||
confirmDeleteCompanyId, setConfirmDeleteCompanyId,
|
||||
setCompany, deleteCompany,
|
||||
section, setSection, error, consoleOpen, setConsoleOpen,
|
||||
tasks, runs, approvals, pendingApprovals,
|
||||
companyAgents, companyGoals, companyProjects, companyWorkspaces,
|
||||
companyCosts, filteredActivity, companySecrets, companyDocuments,
|
||||
companyKnowledge, companyMeetings, companySprints,
|
||||
socialAccounts, browserActions, inboxItems, companyMetrics, standupReport,
|
||||
selectedTask, selectedTaskId, setSelectedTaskId,
|
||||
selectedConnector, setSelectedConnectorId,
|
||||
selectedApproval, selectedApprovalId, setSelectedApprovalId,
|
||||
selectedAgent, selectedAgentId, setSelectedAgentId,
|
||||
selectedGoal, selectedGoalId, setSelectedGoalId,
|
||||
selectedProject, selectedProjectId, setSelectedProjectId,
|
||||
selectedWorkspace, setSelectedWorkspaceId,
|
||||
selectedCommunicationEntityId, setSelectedCommunicationEntityId,
|
||||
selectedRun, selectedRunId, setSelectedRunId,
|
||||
taskViewMode, setTaskViewMode,
|
||||
logBufferRef,
|
||||
navigateTo, refresh, startRun, cancelRun, openPathWithFeedback,
|
||||
commandPaletteOpen, setCommandPaletteOpen,
|
||||
commandPaletteSearch, setCommandPaletteSearch,
|
||||
selectedPaletteIndex, setSelectedPaletteIndex,
|
||||
paletteTrapRef,
|
||||
filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults,
|
||||
paletteActions, paletteActionIndex,
|
||||
// UI / navigation
|
||||
pageTransition: ui.pageTransition,
|
||||
panelTransition: ui.panelTransition,
|
||||
locale: ui.locale,
|
||||
t: ui.t,
|
||||
handleLocaleChange: ui.handleLocaleChange,
|
||||
sidebarNav: ui.sidebarNav,
|
||||
navItems: ui.navItems,
|
||||
section: ui.section,
|
||||
setSection: ui.setSection,
|
||||
consoleOpen: ui.consoleOpen,
|
||||
setConsoleOpen: ui.setConsoleOpen,
|
||||
sidebarCollapsed: ui.sidebarCollapsed,
|
||||
setSidebarCollapsed: ui.setSidebarCollapsed,
|
||||
companySwitcherOpen: ui.companySwitcherOpen,
|
||||
setCompanySwitcherOpen: ui.setCompanySwitcherOpen,
|
||||
confirmDeleteCompanyId: ui.confirmDeleteCompanyId,
|
||||
setConfirmDeleteCompanyId: ui.setConfirmDeleteCompanyId,
|
||||
taskViewMode: ui.taskViewMode,
|
||||
setTaskViewMode: ui.setTaskViewMode,
|
||||
showOnboarding: ui.showOnboarding,
|
||||
setShowOnboarding: ui.setShowOnboarding,
|
||||
initialOnboardingLocked: ui.initialOnboardingLocked,
|
||||
setInitialOnboardingLocked: ui.setInitialOnboardingLocked,
|
||||
commandPaletteOpen: ui.commandPaletteOpen,
|
||||
setCommandPaletteOpen: ui.setCommandPaletteOpen,
|
||||
commandPaletteSearch: ui.commandPaletteSearch,
|
||||
setCommandPaletteSearch: ui.setCommandPaletteSearch,
|
||||
selectedPaletteIndex: ui.selectedPaletteIndex,
|
||||
setSelectedPaletteIndex: ui.setSelectedPaletteIndex,
|
||||
paletteTrapRef: ui.paletteTrapRef,
|
||||
filteredNavItems,
|
||||
matchedAgents,
|
||||
matchedTasks,
|
||||
matchedRuns,
|
||||
matchedGoals,
|
||||
matchedProjects,
|
||||
hasEntityResults,
|
||||
paletteActions,
|
||||
paletteActionIndex,
|
||||
navigateTo,
|
||||
|
||||
// Company / core
|
||||
snapshot: company.snapshot,
|
||||
currentCompany: company.currentCompany,
|
||||
selectedConnector: company.selectedConnector,
|
||||
logBufferRef: company.logBufferRef,
|
||||
refresh: company.refresh,
|
||||
setCompany: company.setCompany,
|
||||
deleteCompany: company.deleteCompany,
|
||||
handleOnboardingDone: company.handleOnboardingDone,
|
||||
openPathWithFeedback: company.openPathWithFeedback,
|
||||
tasks: company.tasks,
|
||||
runs: company.runs,
|
||||
approvals: company.approvals,
|
||||
pendingApprovals: company.pendingApprovals,
|
||||
companyAgents: company.companyAgents,
|
||||
companyGoals: company.companyGoals,
|
||||
companyProjects: company.companyProjects,
|
||||
companyWorkspaces: company.companyWorkspaces,
|
||||
companyCosts: company.companyCosts,
|
||||
filteredActivity: company.filteredActivity,
|
||||
companySecrets: company.companySecrets,
|
||||
companyDocuments: company.companyDocuments,
|
||||
companyKnowledge: company.companyKnowledge,
|
||||
companyMeetings: company.companyMeetings,
|
||||
companySprints: company.companySprints,
|
||||
selectedTask: company.selectedTask,
|
||||
selectedApproval: company.selectedApproval,
|
||||
selectedRun: company.selectedRun,
|
||||
selectedAgent: company.selectedAgent,
|
||||
selectedGoal: company.selectedGoal,
|
||||
selectedProject: company.selectedProject,
|
||||
selectedWorkspace: company.selectedWorkspace,
|
||||
navBadges: company.navBadges,
|
||||
|
||||
// Execution
|
||||
selectedTaskId: execution.selectedTaskId,
|
||||
setSelectedTaskId: execution.setSelectedTaskId,
|
||||
setSelectedConnectorId: execution.setSelectedConnectorId,
|
||||
selectedApprovalId: execution.selectedApprovalId,
|
||||
setSelectedApprovalId: execution.setSelectedApprovalId,
|
||||
selectedRunId: execution.selectedRunId,
|
||||
setSelectedRunId: execution.setSelectedRunId,
|
||||
startRun,
|
||||
cancelRun,
|
||||
|
||||
// Organization
|
||||
selectedAgentId: organization.selectedAgentId,
|
||||
setSelectedAgentId: organization.setSelectedAgentId,
|
||||
selectedGoalId: organization.selectedGoalId,
|
||||
setSelectedGoalId: organization.setSelectedGoalId,
|
||||
selectedProjectId: organization.selectedProjectId,
|
||||
setSelectedProjectId: organization.setSelectedProjectId,
|
||||
setSelectedWorkspaceId: organization.setSelectedWorkspaceId,
|
||||
|
||||
// Communication
|
||||
selectedCommunicationEntityId: communication.selectedCommunicationEntityId,
|
||||
setSelectedCommunicationEntityId: communication.setSelectedCommunicationEntityId,
|
||||
|
||||
// Shared cross-domain state
|
||||
error,
|
||||
setError,
|
||||
inboxItems,
|
||||
companyMetrics,
|
||||
standupReport,
|
||||
socialAccounts,
|
||||
browserActions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useAppStore } from "../state/store";
|
||||
|
||||
/**
|
||||
* Fine-grained subscriptions for communication domain:
|
||||
* messages, channels, threads, search state.
|
||||
*/
|
||||
export function useCommunicationController() {
|
||||
const selectedCommunicationEntityId = useAppStore((s) => s.selectedCommunicationEntityId);
|
||||
const setSelectedCommunicationEntityId = useAppStore((s) => s.setSelectedCommunicationEntityId);
|
||||
const searchQuery = useAppStore((s) => s.searchQuery);
|
||||
const setSearchQuery = useAppStore((s) => s.setSearchQuery);
|
||||
const searchScope = useAppStore((s) => s.searchScope);
|
||||
const setSearchScope = useAppStore((s) => s.setSearchScope);
|
||||
const searchResults = useAppStore((s) => s.searchResults);
|
||||
const setSearchResults = useAppStore((s) => s.setSearchResults);
|
||||
const searchLoading = useAppStore((s) => s.searchLoading);
|
||||
const setSearchLoading = useAppStore((s) => s.setSearchLoading);
|
||||
const clearSearch = useAppStore((s) => s.clearSearch);
|
||||
const activeThreadId = useAppStore((s) => s.activeThreadId);
|
||||
const setActiveThreadId = useAppStore((s) => s.setActiveThreadId);
|
||||
|
||||
return {
|
||||
selectedCommunicationEntityId,
|
||||
setSelectedCommunicationEntityId,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
searchScope,
|
||||
setSearchScope,
|
||||
searchResults,
|
||||
setSearchResults,
|
||||
searchLoading,
|
||||
setSearchLoading,
|
||||
clearSearch,
|
||||
activeThreadId,
|
||||
setActiveThreadId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import type { CompanyRecord, OnboardingBootstrapResult } from "@shared/types";
|
||||
import { useAppStore } from "../state/store";
|
||||
import { NEW_ENTITY_ID } from "../lib/constants";
|
||||
import { unwrap } from "../lib/desktop";
|
||||
import { useDesktopEvents } from "./useDesktopEvents";
|
||||
import { useDataLoader } from "./useDataLoader";
|
||||
import { useCompanyData } from "./useCompanyData";
|
||||
|
||||
export function useCompanyController() {
|
||||
const snapshot = useAppStore((s) => s.snapshot);
|
||||
const setSnapshot = useAppStore((s) => s.setSnapshot);
|
||||
const section = useAppStore((s) => s.section);
|
||||
const setError = useAppStore((s) => s.setError);
|
||||
const setInitialOnboardingLocked = useAppStore((s) => s.setInitialOnboardingLocked);
|
||||
const setSection = useAppStore((s) => s.setSection);
|
||||
const setShowOnboarding = useAppStore((s) => s.setShowOnboarding);
|
||||
const setSelectedRunId = useAppStore((s) => s.setSelectedRunId);
|
||||
const setSelectedAgentId = useAppStore((s) => s.setSelectedAgentId);
|
||||
const setSelectedConnectorId = useAppStore((s) => s.setSelectedConnectorId);
|
||||
const selectedConnectorId = useAppStore((s) => s.selectedConnectorId);
|
||||
const selectedRunId = useAppStore((s) => s.selectedRunId);
|
||||
const setBadges = useAppStore((s) => s.setBadges);
|
||||
const setInboxItems = useAppStore((s) => s.setInboxItems);
|
||||
const setSocialAccounts = useAppStore((s) => s.setSocialAccounts);
|
||||
const setBrowserActions = useAppStore((s) => s.setBrowserActions);
|
||||
const setCompanyMetrics = useAppStore((s) => s.setCompanyMetrics);
|
||||
const setStandupReport = useAppStore((s) => s.setStandupReport);
|
||||
const setConfirmDeleteCompanyId = useAppStore((s) => s.setConfirmDeleteCompanyId);
|
||||
const setCompanySwitcherOpen = useAppStore((s) => s.setCompanySwitcherOpen);
|
||||
const locale = useAppStore((s) => s.locale);
|
||||
const setLocale = useAppStore((s) => s.setLocale);
|
||||
const incrementLogRevision = useAppStore((s) => s.incrementLogRevision);
|
||||
|
||||
const currentCompany = useMemo<CompanyRecord | null>(() => {
|
||||
if (!snapshot) return null;
|
||||
return snapshot.companies.find((company) => company.id === snapshot.currentCompanyId) ?? snapshot.companies[0] ?? null;
|
||||
}, [snapshot]);
|
||||
|
||||
const companyData = useCompanyData(snapshot, currentCompany);
|
||||
|
||||
const selectedConnector = snapshot?.connectors.find((connector) => connector.id === selectedConnectorId) ?? snapshot?.connectors[0] ?? null;
|
||||
|
||||
const clearCompanyScopedUiState = useCallback(() => {
|
||||
const store = useAppStore.getState();
|
||||
store.clearCompanyScopedSelections();
|
||||
store.setInboxItems([]);
|
||||
store.setCompanyMetrics(null);
|
||||
store.setStandupReport(null);
|
||||
store.setSocialAccounts([]);
|
||||
store.setBrowserActions([]);
|
||||
store.setBadges(null);
|
||||
store.setSkills([]);
|
||||
store.setSelectedSkillId(null);
|
||||
store.setSelectedSkillFilePath("SKILL.md");
|
||||
store.setSkillImportModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const { refresh } = useDataLoader(
|
||||
snapshot, section, selectedConnectorId, selectedRunId,
|
||||
setSnapshot, setError, setInitialOnboardingLocked,
|
||||
setSelectedConnectorId, setSelectedRunId,
|
||||
setBadges, setInboxItems, setSocialAccounts, setBrowserActions,
|
||||
setCompanyMetrics, setStandupReport,
|
||||
);
|
||||
|
||||
const logBufferRef = useRef<Record<string, string>>({});
|
||||
|
||||
// Stable ref so the subscribe callback always calls the latest refresh
|
||||
const refreshRef = useRef(refresh);
|
||||
useEffect(() => { refreshRef.current = refresh; }, [refresh]);
|
||||
|
||||
useDesktopEvents(refreshRef, logBufferRef, incrementLogRevision);
|
||||
|
||||
// Theme / locale sync
|
||||
useEffect(() => {
|
||||
if (!snapshot) return;
|
||||
const root = document.documentElement;
|
||||
const applyTheme = () => {
|
||||
const isDark = snapshot.theme === "dark" ||
|
||||
(snapshot.theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
root.classList.toggle("dark", isDark);
|
||||
root.style.colorScheme = isDark ? "dark" : "light";
|
||||
};
|
||||
applyTheme();
|
||||
if (snapshot.locale && snapshot.locale !== locale) setLocale(snapshot.locale);
|
||||
if (snapshot.theme === "system") {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mq.addEventListener("change", applyTheme);
|
||||
return () => mq.removeEventListener("change", applyTheme);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setLocale is a stable Zustand setter
|
||||
}, [snapshot, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale === "zh" ? "zh-CN" : "en";
|
||||
}, [locale]);
|
||||
|
||||
// Keep selectedRunId in sync when run list changes
|
||||
useEffect(() => {
|
||||
if (selectedRunId && !companyData.runs.some((run) => run.id === selectedRunId)) {
|
||||
setSelectedRunId(companyData.runs[0]?.id ?? null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectedRunId is a stable Zustand setter
|
||||
}, [companyData.runs, selectedRunId]);
|
||||
|
||||
// Pre-load run log when active run changes
|
||||
const activeRunId = companyData.selectedRun?.id ?? null;
|
||||
useEffect(() => {
|
||||
if (!activeRunId) return;
|
||||
void (async () => {
|
||||
const activeRun = companyData.runs.find((run) => run.id === activeRunId);
|
||||
if (!activeRun) return;
|
||||
const result = await window.agentCompany.getRunLog({ companyId: activeRun.companyId, runId: activeRunId });
|
||||
if (result.ok) {
|
||||
logBufferRef.current = { ...logBufferRef.current, [activeRunId]: result.data };
|
||||
incrementLogRevision();
|
||||
return;
|
||||
}
|
||||
setError(result.error.message);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- incrementLogRevision and setError are stable Zustand setters
|
||||
}, [activeRunId, companyData.runs]);
|
||||
|
||||
const setCompany = useCallback(async (companyId: string) => {
|
||||
if (companyId === NEW_ENTITY_ID) { setShowOnboarding(true); return; }
|
||||
try {
|
||||
unwrap(await window.agentCompany.setCurrentCompany(companyId));
|
||||
clearCompanyScopedUiState();
|
||||
await refresh();
|
||||
setError(null);
|
||||
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError and setShowOnboarding are stable Zustand setters
|
||||
}, [clearCompanyScopedUiState, refresh]);
|
||||
|
||||
const deleteCompany = useCallback(async (companyId: string) => {
|
||||
try {
|
||||
unwrap(await window.agentCompany.deleteCompany(companyId));
|
||||
setConfirmDeleteCompanyId(null);
|
||||
setCompanySwitcherOpen(false);
|
||||
await refresh();
|
||||
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
|
||||
}, [refresh]);
|
||||
|
||||
const handleOnboardingDone = useCallback(async (summary: OnboardingBootstrapResult) => {
|
||||
setShowOnboarding(false);
|
||||
setInitialOnboardingLocked(false);
|
||||
await refresh();
|
||||
setError(null);
|
||||
setSection("overview");
|
||||
if (summary.runId) setSelectedRunId(summary.runId);
|
||||
if (summary.leadAgentId) setSelectedAgentId(summary.leadAgentId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
|
||||
}, [refresh]);
|
||||
|
||||
const openPathWithFeedback = useCallback(async (path: string) => {
|
||||
try { unwrap(await window.agentCompany.openPath(path)); setError(null); }
|
||||
catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError is a stable Zustand setter
|
||||
}, []);
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
currentCompany,
|
||||
selectedConnector,
|
||||
logBufferRef,
|
||||
refresh,
|
||||
setCompany,
|
||||
deleteCompany,
|
||||
handleOnboardingDone,
|
||||
openPathWithFeedback,
|
||||
...companyData,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useAppStore } from "../state/store";
|
||||
|
||||
/**
|
||||
* Fine-grained subscriptions for execution domain:
|
||||
* runs, approvals, costs, budgets, selected run/approval/task/connector.
|
||||
*
|
||||
* Cross-domain actions (startRun, cancelRun) are composed in useAppController
|
||||
* because they need refresh() from useCompanyController.
|
||||
*/
|
||||
export function useExecutionController() {
|
||||
const selectedTaskId = useAppStore((s) => s.selectedTaskId);
|
||||
const setSelectedTaskId = useAppStore((s) => s.setSelectedTaskId);
|
||||
const selectedConnectorId = useAppStore((s) => s.selectedConnectorId);
|
||||
const setSelectedConnectorId = useAppStore((s) => s.setSelectedConnectorId);
|
||||
const selectedApprovalId = useAppStore((s) => s.selectedApprovalId);
|
||||
const setSelectedApprovalId = useAppStore((s) => s.setSelectedApprovalId);
|
||||
const selectedRunId = useAppStore((s) => s.selectedRunId);
|
||||
const setSelectedRunId = useAppStore((s) => s.setSelectedRunId);
|
||||
|
||||
return {
|
||||
selectedTaskId,
|
||||
setSelectedTaskId,
|
||||
selectedConnectorId,
|
||||
setSelectedConnectorId,
|
||||
selectedApprovalId,
|
||||
setSelectedApprovalId,
|
||||
selectedRunId,
|
||||
setSelectedRunId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useAppStore } from "../state/store";
|
||||
|
||||
/**
|
||||
* Fine-grained subscriptions for organization domain:
|
||||
* goals, projects, sprints, departments, workspaces, agents.
|
||||
*/
|
||||
export function useOrganizationController() {
|
||||
const selectedAgentId = useAppStore((s) => s.selectedAgentId);
|
||||
const setSelectedAgentId = useAppStore((s) => s.setSelectedAgentId);
|
||||
const selectedGoalId = useAppStore((s) => s.selectedGoalId);
|
||||
const setSelectedGoalId = useAppStore((s) => s.setSelectedGoalId);
|
||||
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||
const selectedWorkspaceId = useAppStore((s) => s.selectedWorkspaceId);
|
||||
const setSelectedWorkspaceId = useAppStore((s) => s.setSelectedWorkspaceId);
|
||||
|
||||
return {
|
||||
selectedAgentId,
|
||||
setSelectedAgentId,
|
||||
selectedGoalId,
|
||||
setSelectedGoalId,
|
||||
selectedProjectId,
|
||||
setSelectedProjectId,
|
||||
selectedWorkspaceId,
|
||||
setSelectedWorkspaceId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { useCallback, useDeferredValue, useMemo } from "react";
|
||||
import type { AgentRecord, GoalRecord, ProjectRecord, RunRecord, SectionId, TaskRecord } from "@shared/types";
|
||||
import { createTranslator } from "../i18n";
|
||||
import { useAppStore } from "../state/store";
|
||||
import { useFocusTrap, useReducedMotion } from "../components/ui";
|
||||
import {
|
||||
BookOpen,
|
||||
Gear,
|
||||
Tray,
|
||||
SquaresFour,
|
||||
UsersThree,
|
||||
ListChecks,
|
||||
ChatCircle,
|
||||
PuzzlePiece,
|
||||
FileText,
|
||||
ShieldCheck,
|
||||
Megaphone,
|
||||
FolderOpen,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { AppLocale } from "@shared/locale";
|
||||
|
||||
type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour };
|
||||
export type NavItem = { id: SectionId; label: string; icon: typeof SquaresFour };
|
||||
export type PaletteAction = { key: string; section: SectionId; entityId?: string };
|
||||
|
||||
const sidebarNavDefs: NavItemDef[] = [
|
||||
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
|
||||
{ id: "inbox", labelKey: "nav.inbox", icon: Tray },
|
||||
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
|
||||
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
|
||||
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
|
||||
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
|
||||
{ id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen },
|
||||
{ id: "social", labelKey: "nav.social", icon: Megaphone },
|
||||
{ id: "documents", labelKey: "nav.documents", icon: FileText },
|
||||
];
|
||||
|
||||
const paletteOnlyNavDefs: NavItemDef[] = [
|
||||
{ id: "skills", labelKey: "nav.skills", icon: PuzzlePiece },
|
||||
{ id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen },
|
||||
];
|
||||
|
||||
export function useUIController() {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const noMotion = { duration: 0 };
|
||||
const pageTransition = reducedMotion ? noMotion : { duration: 0.32, ease: [0.16, 1, 0.3, 1] as const };
|
||||
const panelTransition = reducedMotion ? noMotion : { duration: 0.28, ease: [0.16, 1, 0.3, 1] as const };
|
||||
|
||||
const locale = useAppStore((s) => s.locale);
|
||||
const t = useMemo(() => createTranslator(locale), [locale]);
|
||||
|
||||
const sidebarNav: NavItem[] = useMemo(
|
||||
() => sidebarNavDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })),
|
||||
[t],
|
||||
);
|
||||
const navItems: NavItem[] = useMemo(
|
||||
() =>
|
||||
sidebarNav
|
||||
.concat(paletteOnlyNavDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })))
|
||||
.concat({ id: "settings", label: t("nav.settings"), icon: Gear }),
|
||||
[sidebarNav, t],
|
||||
);
|
||||
|
||||
const section = useAppStore((s) => s.section);
|
||||
const setSection = useAppStore((s) => s.setSection);
|
||||
const consoleOpen = useAppStore((s) => s.consoleOpen);
|
||||
const setConsoleOpen = useAppStore((s) => s.setConsoleOpen);
|
||||
const sidebarCollapsed = useAppStore((s) => s.sidebarCollapsed);
|
||||
const setSidebarCollapsed = useAppStore((s) => s.setSidebarCollapsed);
|
||||
const companySwitcherOpen = useAppStore((s) => s.companySwitcherOpen);
|
||||
const setCompanySwitcherOpen = useAppStore((s) => s.setCompanySwitcherOpen);
|
||||
const confirmDeleteCompanyId = useAppStore((s) => s.confirmDeleteCompanyId);
|
||||
const setConfirmDeleteCompanyId = useAppStore((s) => s.setConfirmDeleteCompanyId);
|
||||
const taskViewMode = useAppStore((s) => s.taskViewMode);
|
||||
const setTaskViewMode = useAppStore((s) => s.setTaskViewMode);
|
||||
const showOnboarding = useAppStore((s) => s.showOnboarding);
|
||||
const setShowOnboarding = useAppStore((s) => s.setShowOnboarding);
|
||||
const initialOnboardingLocked = useAppStore((s) => s.initialOnboardingLocked);
|
||||
const setInitialOnboardingLocked = useAppStore((s) => s.setInitialOnboardingLocked);
|
||||
const commandPaletteOpen = useAppStore((s) => s.commandPaletteOpen);
|
||||
const setCommandPaletteOpen = useAppStore((s) => s.setCommandPaletteOpen);
|
||||
const commandPaletteSearch = useAppStore((s) => s.commandPaletteSearch);
|
||||
const setCommandPaletteSearch = useAppStore((s) => s.setCommandPaletteSearch);
|
||||
const selectedPaletteIndex = useAppStore((s) => s.selectedPaletteIndex);
|
||||
const setSelectedPaletteIndex = useAppStore((s) => s.setSelectedPaletteIndex);
|
||||
|
||||
const setSelectedAgentId = useAppStore((s) => s.setSelectedAgentId);
|
||||
const setSelectedConnectorId = useAppStore((s) => s.setSelectedConnectorId);
|
||||
const setSelectedRunId = useAppStore((s) => s.setSelectedRunId);
|
||||
const setSelectedTaskId = useAppStore((s) => s.setSelectedTaskId);
|
||||
const setSelectedApprovalId = useAppStore((s) => s.setSelectedApprovalId);
|
||||
const setSelectedGoalId = useAppStore((s) => s.setSelectedGoalId);
|
||||
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||
const setSelectedCommunicationEntityId = useAppStore((s) => s.setSelectedCommunicationEntityId);
|
||||
|
||||
const handleLocaleChange = useCallback(async (newLocale: AppLocale) => {
|
||||
useAppStore.getState().setLocale(newLocale);
|
||||
try { await window.agentCompany.updateSettings({ locale: newLocale }); } catch { /* local state already updated */ }
|
||||
}, []);
|
||||
|
||||
const paletteTrapRef = useFocusTrap(commandPaletteOpen);
|
||||
|
||||
// These are passed in from useAppController which has access to entity lists
|
||||
// For standalone palette search, we expose the query and let consumers compute results
|
||||
const paletteQuery = useDeferredValue(commandPaletteSearch.trim().toLowerCase());
|
||||
|
||||
const navigateTo = useCallback((targetSection: SectionId, entityId?: string, runs?: RunRecord[], tasks?: TaskRecord[]) => {
|
||||
setSection(targetSection);
|
||||
if (entityId) {
|
||||
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||
else if (targetSection === "connectors") setSelectedConnectorId(entityId);
|
||||
else if (targetSection === "runs") { setSelectedRunId(entityId); setConsoleOpen(true); }
|
||||
else if (targetSection === "tasks") {
|
||||
const matchingTask = tasks?.find((task) => task.id === entityId) ?? null;
|
||||
if (matchingTask) { setSelectedTaskId(matchingTask.id); }
|
||||
else {
|
||||
const matchingRun = runs?.find((run) => run.id === entityId) ?? null;
|
||||
if (matchingRun) { setSelectedRunId(matchingRun.id); setSelectedTaskId(matchingRun.taskId); setConsoleOpen(true); }
|
||||
}
|
||||
}
|
||||
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||
else if (targetSection === "communication") setSelectedCommunicationEntityId(entityId);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
|
||||
}, []);
|
||||
|
||||
const buildPaletteResults = useCallback((
|
||||
navItemsList: NavItem[],
|
||||
companyAgents: AgentRecord[],
|
||||
tasks: TaskRecord[],
|
||||
runs: RunRecord[],
|
||||
companyGoals: GoalRecord[],
|
||||
companyProjects: ProjectRecord[],
|
||||
) => {
|
||||
const filteredNavItems = paletteQuery
|
||||
? navItemsList.filter((item) => item.label.toLowerCase().includes(paletteQuery) || item.id.toLowerCase().includes(paletteQuery))
|
||||
: navItemsList;
|
||||
const matchedAgents = paletteQuery ? companyAgents.filter((a) => a.name.toLowerCase().includes(paletteQuery) || a.role.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const matchedTasks = paletteQuery ? tasks.filter((task) => task.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const matchedRuns = paletteQuery ? runs.filter((run) => {
|
||||
const matchingTask = tasks.find((task) => task.id === run.taskId);
|
||||
return [run.id, run.summary ?? "", run.errorMessage ?? "", matchingTask?.title ?? "", run.connectorId]
|
||||
.some((value) => value.toLowerCase().includes(paletteQuery));
|
||||
}).slice(0, 5) : [];
|
||||
const matchedGoals = paletteQuery ? companyGoals.filter((g) => g.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const matchedProjects = paletteQuery ? companyProjects.filter((p) => p.name.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
|
||||
const hasEntityResults = matchedAgents.length > 0 || matchedTasks.length > 0 || matchedRuns.length > 0 || matchedGoals.length > 0 || matchedProjects.length > 0;
|
||||
return { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults };
|
||||
}, [paletteQuery]);
|
||||
|
||||
const buildPaletteActions = useCallback((
|
||||
filteredNavItems: NavItem[],
|
||||
matchedAgents: AgentRecord[],
|
||||
matchedTasks: TaskRecord[],
|
||||
matchedRuns: RunRecord[],
|
||||
matchedGoals: GoalRecord[],
|
||||
matchedProjects: ProjectRecord[],
|
||||
): PaletteAction[] => ([
|
||||
...filteredNavItems.map((item) => ({ key: `section:${item.id}`, section: item.id })),
|
||||
...matchedAgents.map((agent) => ({ key: `agent:${agent.id}`, section: "agents" as const, entityId: agent.id })),
|
||||
...matchedTasks.map((task) => ({ key: `task:${task.id}`, section: "tasks" as const, entityId: task.id })),
|
||||
...matchedRuns.map((run) => ({ key: `run:${run.id}`, section: "runs" as const, entityId: run.id })),
|
||||
...matchedGoals.map((goal) => ({ key: `goal:${goal.id}`, section: "goals" as const, entityId: goal.id })),
|
||||
...matchedProjects.map((project) => ({ key: `project:${project.id}`, section: "projects" as const, entityId: project.id })),
|
||||
]), []);
|
||||
|
||||
return {
|
||||
pageTransition,
|
||||
panelTransition,
|
||||
locale,
|
||||
t,
|
||||
handleLocaleChange,
|
||||
sidebarNav,
|
||||
navItems,
|
||||
section,
|
||||
setSection,
|
||||
consoleOpen,
|
||||
setConsoleOpen,
|
||||
sidebarCollapsed,
|
||||
setSidebarCollapsed,
|
||||
companySwitcherOpen,
|
||||
setCompanySwitcherOpen,
|
||||
confirmDeleteCompanyId,
|
||||
setConfirmDeleteCompanyId,
|
||||
taskViewMode,
|
||||
setTaskViewMode,
|
||||
showOnboarding,
|
||||
setShowOnboarding,
|
||||
initialOnboardingLocked,
|
||||
setInitialOnboardingLocked,
|
||||
commandPaletteOpen,
|
||||
setCommandPaletteOpen,
|
||||
commandPaletteSearch,
|
||||
setCommandPaletteSearch,
|
||||
selectedPaletteIndex,
|
||||
setSelectedPaletteIndex,
|
||||
paletteQuery,
|
||||
paletteTrapRef,
|
||||
navigateTo,
|
||||
buildPaletteResults,
|
||||
buildPaletteActions,
|
||||
};
|
||||
}
|
||||
@@ -703,6 +703,41 @@ export const en: Record<string, string> = {
|
||||
"org.model": "Model",
|
||||
"org.interval": "Interval",
|
||||
"org.fit": "Fit",
|
||||
"org.description": "Description",
|
||||
"org.status": "Status",
|
||||
"org.statusPlanned": "Planned",
|
||||
"org.statusActive": "Active",
|
||||
"org.statusAchieved": "Achieved",
|
||||
"org.statusCancelled": "Cancelled",
|
||||
"org.connector": "Connector",
|
||||
"org.advancedSettings": "Advanced settings",
|
||||
"org.hireRequestNote": "New agents enter the hiring pipeline first. Approval activates the agent and keeps onboarding, budgets, and org history linked to the same record.",
|
||||
"org.connectorLabel": "Connector:",
|
||||
"org.workspaceLabel": "Workspace:",
|
||||
"org.goalLabel": "Goal:",
|
||||
"org.leadLabel": "Lead:",
|
||||
"org.targetLabel": "Target:",
|
||||
"org.workspaceCount": "{0} workspace",
|
||||
"org.workspacesCount": "{0} workspaces",
|
||||
"org.noConnectorAvailable": "No execution-ready connector is available. Fix connector health before requesting hires.",
|
||||
"org.selectConnectorFirst": "Select an execution-ready connector before requesting a hire.",
|
||||
"org.hireRequestFailed": "Hire request failed.",
|
||||
"dash.activeWork": "Active Work",
|
||||
"dash.allRuns": "All runs",
|
||||
"dash.noTasksRunning": "No tasks currently running",
|
||||
"dash.agentActive": "agent active",
|
||||
"dash.agentsActive": "agents active",
|
||||
"dash.taskInProgress": "task in progress",
|
||||
"dash.tasksInProgress": "tasks in progress",
|
||||
"dash.completedCount": "completed",
|
||||
"exec.taskDescription": "Description",
|
||||
"exec.describeTask": "Describe the task",
|
||||
"social.addAccountLabel": "Add Account",
|
||||
"social.howItWorksTitle": "How it works",
|
||||
"social.accountsTab": "Accounts ({0})",
|
||||
"social.actionHistoryTab": "Action History ({0})",
|
||||
"social.accountsConnectedCount": "{0} account connected",
|
||||
"social.accountsConnectedCountPlural": "{0} accounts connected",
|
||||
|
||||
// ── Runs ──
|
||||
"runs.noRunsYet": "No agent runs yet",
|
||||
|
||||
@@ -703,6 +703,41 @@ export const zh: Record<string, string> = {
|
||||
"org.model": "模型",
|
||||
"org.interval": "间隔",
|
||||
"org.fit": "适配",
|
||||
"org.description": "描述",
|
||||
"org.status": "状态",
|
||||
"org.statusPlanned": "已规划",
|
||||
"org.statusActive": "活跃",
|
||||
"org.statusAchieved": "已达成",
|
||||
"org.statusCancelled": "已取消",
|
||||
"org.connector": "连接器",
|
||||
"org.advancedSettings": "高级设置",
|
||||
"org.hireRequestNote": "新智能体先进招聘管线。审批通过后激活,入职、预算和组织历史记录全部挂在同一条记录下。",
|
||||
"org.connectorLabel": "连接器:",
|
||||
"org.workspaceLabel": "工作区:",
|
||||
"org.goalLabel": "目标:",
|
||||
"org.leadLabel": "负责人:",
|
||||
"org.targetLabel": "目标日期:",
|
||||
"org.workspaceCount": "{0} 个工作区",
|
||||
"org.workspacesCount": "{0} 个工作区",
|
||||
"org.noConnectorAvailable": "暂无可用的执行连接器,请先修复连接器状态再发起招聘。",
|
||||
"org.selectConnectorFirst": "请先选择一个可用的执行连接器再发起招聘。",
|
||||
"org.hireRequestFailed": "招聘申请失败。",
|
||||
"dash.activeWork": "当前工作",
|
||||
"dash.allRuns": "全部运行",
|
||||
"dash.noTasksRunning": "当前没有任务在运行",
|
||||
"dash.agentActive": "个智能体活跃",
|
||||
"dash.agentsActive": "个智能体活跃",
|
||||
"dash.taskInProgress": "个任务进行中",
|
||||
"dash.tasksInProgress": "个任务进行中",
|
||||
"dash.completedCount": "已完成",
|
||||
"exec.taskDescription": "描述",
|
||||
"exec.describeTask": "描述任务",
|
||||
"social.addAccountLabel": "添加账号",
|
||||
"social.howItWorksTitle": "使用说明",
|
||||
"social.accountsTab": "账号({0})",
|
||||
"social.actionHistoryTab": "操作历史({0})",
|
||||
"social.accountsConnectedCount": "已连接 {0} 个账号",
|
||||
"social.accountsConnectedCountPlural": "已连接 {0} 个账号",
|
||||
|
||||
// ── 运行 ──
|
||||
"runs.noRunsYet": "暂无智能体运行记录",
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!--
|
||||
CSP: 'unsafe-inline' in style-src is required because:
|
||||
1. Framer Motion injects <style> elements at runtime (AnimatePresence/PopChild)
|
||||
2. OnboardingHero.tsx uses inline <style> for @keyframes
|
||||
3. Tailwind CSS v4 Vite plugin injects <style> during dev mode (HMR)
|
||||
Removing it breaks animations and dev workflow. Nonce-based CSP is not feasible
|
||||
because Framer Motion's runtime style injection does not support nonces.
|
||||
-->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self';"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Sentinel value used as entity ID to indicate "create new" in UI state. */
|
||||
export const NEW_ENTITY_ID = "__new__" as const;
|
||||
@@ -86,7 +86,7 @@ export function ConsoleView({
|
||||
</div>
|
||||
{selectedRun.logPath ? (
|
||||
<div className="pt-1">
|
||||
<ActionButton label={t("common.openLog")} onClick={() => void openPathWithFeedback(selectedRun.logPath!)} icon={FolderOpen} />
|
||||
<ActionButton label={t("common.openLog")} onClick={() => { const p = selectedRun.logPath; if (p) void openPathWithFeedback(p); }} icon={FolderOpen} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@phosphor-icons/react";
|
||||
import type { CompanyRecord, ProfileSnapshot, SectionId } from "@shared/types";
|
||||
import { SectionButton } from "../components/ui";
|
||||
import { NEW_ENTITY_ID } from "../lib/constants";
|
||||
|
||||
type NavItem = { id: SectionId; label: string; icon: typeof Gear };
|
||||
|
||||
@@ -129,7 +130,7 @@ export function SidebarView({
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-[13px] text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
||||
onClick={() => { void setCompany("__new__"); setCompanySwitcherOpen(false); }}
|
||||
onClick={() => { void setCompany(NEW_ENTITY_ID); setCompanySwitcherOpen(false); }}
|
||||
>
|
||||
<Plus size={14} />
|
||||
<span>{t("app.newCompany")}</span>
|
||||
@@ -168,7 +169,7 @@ export function SidebarView({
|
||||
<div className={`border-t border-border py-2 ${sidebarCollapsed ? "px-1.5" : "px-3"}`}>
|
||||
<SectionButton
|
||||
active={section === "settings"}
|
||||
label="Settings"
|
||||
label={t("nav.settings")}
|
||||
icon={Gear}
|
||||
onClick={() => setSection("settings")}
|
||||
collapsed={sidebarCollapsed}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { NEW_ENTITY_ID } from "../../lib/constants";
|
||||
import type {
|
||||
AgentRecord,
|
||||
ApprovalRecord,
|
||||
@@ -123,6 +124,8 @@ export function ExecutionSections({
|
||||
const [costsTab, setCostsTab] = useState<"costs" | "policies" | "incidents">("costs");
|
||||
const selectedGoal = companyGoals.find((g) => g.id === selectedGoalId) ?? null;
|
||||
|
||||
if (!currentCompany) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{section === "runs" ? (
|
||||
@@ -179,11 +182,11 @@ export function ExecutionSections({
|
||||
|
||||
{section === "tasks" ? (
|
||||
<>
|
||||
<Panel title="Goals" action={<ActionButton label="New goal" icon={Target} tone="accent" onClick={() => setSelectedGoalId("__new__")} />}>
|
||||
<Panel title="Goals" action={<ActionButton label="New goal" icon={Target} tone="accent" onClick={() => setSelectedGoalId(NEW_ENTITY_ID)} />}>
|
||||
<GoalList goals={companyGoals} agents={companyAgents} selectedGoalId={selectedGoalId} onSelect={setSelectedGoalId} />
|
||||
</Panel>
|
||||
<Modal open={selectedGoalId === "__new__" && section === "tasks"} title="New goal" onClose={() => setSelectedGoalId(null)}>
|
||||
<GoalCreator companyId={currentCompany!.id} agents={companyAgents} goals={companyGoals} onSaved={async () => { setSelectedGoalId(null); await refresh(); }} />
|
||||
<Modal open={selectedGoalId === NEW_ENTITY_ID && section === "tasks"} title="New goal" onClose={() => setSelectedGoalId(null)}>
|
||||
<GoalCreator companyId={currentCompany.id} agents={companyAgents} goals={companyGoals} onSaved={async () => { setSelectedGoalId(null); await refresh(); }} />
|
||||
</Modal>
|
||||
<Modal open={!!selectedGoal && section === "tasks"} title={selectedGoal ? `Edit: ${selectedGoal.title}` : ""} onClose={() => setSelectedGoalId(null)}>
|
||||
{selectedGoal ? (
|
||||
@@ -192,7 +195,7 @@ export function ExecutionSections({
|
||||
goal={selectedGoal}
|
||||
goals={companyGoals}
|
||||
agents={companyAgents}
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
onSaved={async () => { setSelectedGoalId(null); await refresh(); }}
|
||||
onDeleted={async () => { setSelectedGoalId(null); await refresh(); }}
|
||||
/>
|
||||
@@ -219,7 +222,7 @@ export function ExecutionSections({
|
||||
<Kanban size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<ActionButton label="New task" icon={ListChecks} tone="accent" onClick={() => setSelectedTaskId("__new__")} />
|
||||
<ActionButton label="New task" icon={ListChecks} tone="accent" onClick={() => setSelectedTaskId(NEW_ENTITY_ID)} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -229,8 +232,8 @@ export function ExecutionSections({
|
||||
<TaskQueue tasks={tasks} runs={runs} onSelect={setSelectedTaskId} selectedTaskId={selectedTask?.id ?? null} />
|
||||
)}
|
||||
</Panel>
|
||||
<Modal open={selectedTaskId === "__new__"} title="New task" onClose={() => setSelectedTaskId(null)}>
|
||||
<TaskCreator companyId={currentCompany!.id} goals={companyGoals} projects={companyProjects} agents={companyAgents} workspaces={companyWorkspaces} onSaved={async () => { setSelectedTaskId(null); await refresh(); }} />
|
||||
<Modal open={selectedTaskId === NEW_ENTITY_ID} title="New task" onClose={() => setSelectedTaskId(null)}>
|
||||
<TaskCreator companyId={currentCompany.id} goals={companyGoals} projects={companyProjects} agents={companyAgents} workspaces={companyWorkspaces} onSaved={async () => { setSelectedTaskId(null); await refresh(); }} />
|
||||
</Modal>
|
||||
<Modal open={!!selectedTask} title={selectedTask?.title ?? ""} onClose={() => setSelectedTaskId(null)} width="720px">
|
||||
{selectedTask ? (
|
||||
@@ -242,7 +245,7 @@ export function ExecutionSections({
|
||||
goals={companyGoals}
|
||||
projects={companyProjects}
|
||||
workspaces={companyWorkspaces}
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
comments={(snapshot.comments ?? []).filter((comment) => comment.taskId === selectedTask.id)}
|
||||
onRefresh={refresh}
|
||||
onRun={startRun}
|
||||
@@ -250,7 +253,7 @@ export function ExecutionSections({
|
||||
onCreateApproval={async () => {
|
||||
const isBlocked = selectedTask.status === "blocked";
|
||||
const approvalId = unwrap(await window.agentCompany.requestApproval({
|
||||
companyId: currentCompany!.id,
|
||||
companyId: currentCompany.id,
|
||||
relatedTaskId: selectedTask.id,
|
||||
requestedByAgentId: selectedTask.assigneeAgentId,
|
||||
type: isBlocked ? "secret_access" : "approve_ceo_strategy",
|
||||
@@ -291,9 +294,9 @@ export function ExecutionSections({
|
||||
agents={companyAgents}
|
||||
tasks={tasks}
|
||||
documents={companyDocuments}
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
onDecide={async (state, note) => {
|
||||
unwrap(await window.agentCompany.decideApproval({ companyId: currentCompany!.id, 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(); }}
|
||||
@@ -338,13 +341,13 @@ export function ExecutionSections({
|
||||
/>
|
||||
) : costsTab === "policies" ? (
|
||||
<PolicyList
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
agents={companyAgents.map((a) => ({ id: a.id, name: a.name }))}
|
||||
projects={companyProjects.map((p) => ({ id: p.id, name: p.name }))}
|
||||
/>
|
||||
) : (
|
||||
<IncidentList
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
agents={companyAgents.map((a) => ({ id: a.id, name: a.name }))}
|
||||
projects={companyProjects.map((p) => ({ id: p.id, name: p.name }))}
|
||||
/>
|
||||
|
||||
@@ -284,7 +284,7 @@ export function OperationsSections({
|
||||
<WorkspaceList
|
||||
workspaces={companyWorkspaces}
|
||||
projects={companyProjects}
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
selectedWorkspaceId={selectedWorkspace?.id ?? null}
|
||||
onSelect={setSelectedWorkspaceId}
|
||||
onOpen={openPathWithFeedback}
|
||||
@@ -306,7 +306,7 @@ export function OperationsSections({
|
||||
/>
|
||||
</Panel>
|
||||
<Panel title="Add workspace">
|
||||
<WorkspaceCreator companyId={currentCompany!.id} projects={companyProjects} onSaved={refresh} />
|
||||
<WorkspaceCreator companyId={currentCompany.id} projects={companyProjects} onSaved={refresh} />
|
||||
</Panel>
|
||||
</div>
|
||||
}
|
||||
@@ -316,7 +316,7 @@ export function OperationsSections({
|
||||
{section === "settings" && currentCompany ? (
|
||||
<div className="space-y-10">
|
||||
<Panel title={locale === "zh" ? "配置" : "Profile and release"}>
|
||||
<SettingsPane snapshot={snapshot} secrets={companySecrets} companyId={currentCompany!.id} onSaved={refresh} />
|
||||
<SettingsPane snapshot={snapshot} secrets={companySecrets} companyId={currentCompany.id} onSaved={refresh} />
|
||||
</Panel>
|
||||
<Panel title={locale === "zh" ? "模型管理" : "Model Management"}>
|
||||
<ModelManagement connectors={snapshot.connectors} locale={locale} onSaved={refresh} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ProfileSnapshot,
|
||||
SectionId,
|
||||
} from "@shared/types";
|
||||
import { NEW_ENTITY_ID } from "../../lib/constants";
|
||||
import {
|
||||
Target,
|
||||
UserPlus,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from "../../components/organization";
|
||||
import { HiringPipeline } from "../../components/hiring";
|
||||
import { ActionButton, Modal, Panel } from "../../components/ui";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
interface OrganizationSectionsProps {
|
||||
section: SectionId;
|
||||
@@ -62,16 +64,19 @@ export function OrganizationSections({
|
||||
setSection,
|
||||
refresh,
|
||||
}: OrganizationSectionsProps) {
|
||||
const t = useT();
|
||||
const selectedAgent = companyAgents.find((a) => a.id === selectedAgentId) ?? null;
|
||||
const selectedGoal = companyGoals.find((g) => g.id === selectedGoalId) ?? null;
|
||||
const selectedProject = companyProjects.find((p) => p.id === selectedProjectId) ?? null;
|
||||
|
||||
if (!currentCompany) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{section === "agents" ? (
|
||||
<>
|
||||
<OrgChart agents={companyAgents} connectors={snapshot.connectors} onSelectAgent={(id) => setSelectedAgentId(id)} />
|
||||
<Panel title="Agent roster" action={<ActionButton label="New hire request" icon={UserPlus} tone="accent" onClick={() => setSelectedAgentId("__new__")} />}>
|
||||
<Panel title={t("org.agentRoster")} action={<ActionButton label={t("org.newHireRequest")} icon={UserPlus} tone="accent" onClick={() => setSelectedAgentId(NEW_ENTITY_ID)} />}>
|
||||
<AgentRoster
|
||||
agents={companyAgents}
|
||||
connectors={snapshot.connectors}
|
||||
@@ -80,9 +85,9 @@ export function OrganizationSections({
|
||||
onSelect={setSelectedAgentId}
|
||||
/>
|
||||
</Panel>
|
||||
<Modal open={selectedAgentId === "__new__"} title="Request new hire" onClose={() => setSelectedAgentId(null)}>
|
||||
<Modal open={selectedAgentId === NEW_ENTITY_ID} title={t("org.requestNewHire")} onClose={() => setSelectedAgentId(null)}>
|
||||
<AgentCreator
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
connectors={snapshot.connectors}
|
||||
workspaces={companyWorkspaces}
|
||||
agents={companyAgents}
|
||||
@@ -101,7 +106,7 @@ export function OrganizationSections({
|
||||
connectors={snapshot.connectors}
|
||||
workspaces={companyWorkspaces}
|
||||
agents={companyAgents}
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
onSaved={refresh}
|
||||
onDeleted={async () => { setSelectedAgentId(null); await refresh(); }}
|
||||
/>
|
||||
@@ -117,7 +122,7 @@ export function OrganizationSections({
|
||||
{section === "hiring" && currentCompany ? (
|
||||
<HiringPipeline
|
||||
agents={companyAgents}
|
||||
approvals={(snapshot?.approvals ?? []).filter(a => a.companyId === currentCompany!.id)}
|
||||
approvals={(snapshot?.approvals ?? []).filter(a => a.companyId === currentCompany.id)}
|
||||
onRefresh={refresh}
|
||||
onNavigateToAgent={(id) => { setSelectedAgentId(id); setSection("agents"); }}
|
||||
/>
|
||||
@@ -125,11 +130,11 @@ export function OrganizationSections({
|
||||
|
||||
{section === "goals" ? (
|
||||
<>
|
||||
<Panel title="Goal stack" action={<ActionButton label="New goal" icon={Target} tone="accent" onClick={() => setSelectedGoalId("__new__")} />}>
|
||||
<Panel title={t("org.goalStack")} action={<ActionButton label={t("org.newGoal")} icon={Target} tone="accent" onClick={() => setSelectedGoalId(NEW_ENTITY_ID)} />}>
|
||||
<GoalList goals={companyGoals} agents={companyAgents} selectedGoalId={selectedGoalId} onSelect={setSelectedGoalId} />
|
||||
</Panel>
|
||||
<Modal open={selectedGoalId === "__new__"} title="New goal" onClose={() => setSelectedGoalId(null)}>
|
||||
<GoalCreator companyId={currentCompany!.id} agents={companyAgents} goals={companyGoals} onSaved={async () => { setSelectedGoalId(null); await refresh(); }} />
|
||||
<Modal open={selectedGoalId === NEW_ENTITY_ID} title={t("org.newGoal")} onClose={() => setSelectedGoalId(null)}>
|
||||
<GoalCreator companyId={currentCompany.id} agents={companyAgents} goals={companyGoals} onSaved={async () => { setSelectedGoalId(null); await refresh(); }} />
|
||||
</Modal>
|
||||
<Modal open={!!selectedGoal} title={selectedGoal ? `Edit: ${selectedGoal.title}` : ""} onClose={() => setSelectedGoalId(null)}>
|
||||
{selectedGoal ? (
|
||||
@@ -138,7 +143,7 @@ export function OrganizationSections({
|
||||
goal={selectedGoal}
|
||||
goals={companyGoals}
|
||||
agents={companyAgents}
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
onSaved={async () => { setSelectedGoalId(null); await refresh(); }}
|
||||
onDeleted={async () => { setSelectedGoalId(null); await refresh(); }}
|
||||
/>
|
||||
@@ -149,11 +154,11 @@ export function OrganizationSections({
|
||||
|
||||
{section === "projects" ? (
|
||||
<>
|
||||
<Panel title="Projects" action={<ActionButton label="New project" icon={Kanban} tone="accent" onClick={() => setSelectedProjectId("__new__")} />}>
|
||||
<Panel title={t("nav.projects")} action={<ActionButton label={t("org.newProject")} icon={Kanban} tone="accent" onClick={() => setSelectedProjectId(NEW_ENTITY_ID)} />}>
|
||||
<ProjectList projects={companyProjects} goals={companyGoals} agents={companyAgents} workspaces={companyWorkspaces} selectedProjectId={selectedProjectId} onSelect={setSelectedProjectId} />
|
||||
</Panel>
|
||||
<Modal open={selectedProjectId === "__new__"} title="New project" onClose={() => setSelectedProjectId(null)}>
|
||||
<ProjectCreator companyId={currentCompany!.id} goals={companyGoals} agents={companyAgents} onSaved={async () => { setSelectedProjectId(null); await refresh(); }} />
|
||||
<Modal open={selectedProjectId === NEW_ENTITY_ID} title={t("org.newProject")} onClose={() => setSelectedProjectId(null)}>
|
||||
<ProjectCreator companyId={currentCompany.id} goals={companyGoals} agents={companyAgents} onSaved={async () => { setSelectedProjectId(null); await refresh(); }} />
|
||||
</Modal>
|
||||
<Modal open={!!selectedProject} title={selectedProject ? `Edit: ${selectedProject.name}` : ""} onClose={() => setSelectedProjectId(null)}>
|
||||
{selectedProject ? (
|
||||
@@ -162,7 +167,7 @@ export function OrganizationSections({
|
||||
project={selectedProject}
|
||||
goals={companyGoals}
|
||||
agents={companyAgents}
|
||||
companyId={currentCompany!.id}
|
||||
companyId={currentCompany.id}
|
||||
onSaved={async () => { setSelectedProjectId(null); await refresh(); }}
|
||||
onDeleted={async () => { setSelectedProjectId(null); await refresh(); }}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user