This commit is contained in:
ZacharyZhang-NY committed 2026-04-05 20:14:48 -04:00
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";
-5
View File
@@ -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} />
@@ -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>
@@ -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}
@@ -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)}
@@ -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}
/>
)}
@@ -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}
/>
)}
@@ -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;
@@ -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);
}
-1
View File
@@ -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>
+1 -1
View File
@@ -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"