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"
|
||||
|
||||
Reference in new issue
Block a user