Compare commits
13 commits
4c8cfcd851
...
bb393b421d
| Author | SHA1 | Date | |
|---|---|---|---|
| bb393b421d | |||
| 9710478d6d | |||
| e647a6fba2 | |||
| 560400b187 | |||
| f0f65a63dd | |||
| c2434bc67e | |||
| 9de10c7161 | |||
| bf3215cc87 | |||
| 31c9fe8671 | |||
| cb5d14d6f8 | |||
| 02282ae926 | |||
| aba86d5a7c | |||
| 2e7a273687 |
59 changed files with 414 additions and 351 deletions
|
|
@ -4,6 +4,7 @@ import {
|
|||
readAgentJwtSecretFromEnvFile,
|
||||
resolveAgentJwtEnvFile,
|
||||
} from "../config/env.js";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import type { CheckResult } from "./index.js";
|
||||
|
||||
export function agentJwtSecretCheck(configPath?: string): CheckResult {
|
||||
|
|
@ -23,7 +24,7 @@ export function agentJwtSecretCheck(configPath?: string): CheckResult {
|
|||
name: "Agent JWT secret",
|
||||
status: "warn",
|
||||
message: `PAPERCLIP_AGENT_JWT_SECRET is present in ${envPath} but not loaded into environment`,
|
||||
repairHint: `Set the value from ${envPath} in your shell before starting the Paperclip server`,
|
||||
repairHint: `Set the value from ${envPath} in your shell before starting the ${VOCAB.appName} server`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { PaperclipConfig } from "../config/schema.js";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import type { CheckResult } from "./index.js";
|
||||
|
||||
function isLoopbackHost(host: string) {
|
||||
|
|
@ -37,7 +38,7 @@ export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
|
|||
status: "fail",
|
||||
message: "authenticated mode requires BETTER_AUTH_SECRET (or PAPERCLIP_AGENT_JWT_SECRET)",
|
||||
canRepair: false,
|
||||
repairHint: "Set BETTER_AUTH_SECRET before starting Paperclip",
|
||||
repairHint: `Set BETTER_AUTH_SECRET before starting ${VOCAB.appName}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as p from "@clack/prompts";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import pc from "picocolors";
|
||||
import { normalizeHostnameInput } from "../config/hostnames.js";
|
||||
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
||||
|
|
@ -27,7 +28,7 @@ export async function addAllowedHostname(host: string, opts: { config?: string }
|
|||
} else {
|
||||
p.log.success(`Added allowed hostname: ${pc.cyan(normalized)}`);
|
||||
p.log.message(
|
||||
pc.dim("Restart the Paperclip server for this change to take effect."),
|
||||
pc.dim(`Restart the ${VOCAB.appName} server for this change to take effect.`),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Command } from "commander";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import type { ActivityEvent } from "@paperclipai/shared";
|
||||
import {
|
||||
addCommonClientOptions,
|
||||
|
|
@ -22,8 +23,8 @@ export function registerActivityCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
activity
|
||||
.command("list")
|
||||
.description("List company activity log entries")
|
||||
.requiredOption("-C, --company-id <id>", "Company ID")
|
||||
.description(`List ${VOCAB.company.toLowerCase()} activity log entries`)
|
||||
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.option("--agent-id <id>", "Filter by agent ID")
|
||||
.option("--entity-type <type>", "Filter by entity type")
|
||||
.option("--entity-id <id>", "Filter by entity ID")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Command } from "commander";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
import {
|
||||
removeMaintainerOnlySkillSymlinks,
|
||||
|
|
@ -162,8 +163,8 @@ export function registerAgentCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
agent
|
||||
.command("list")
|
||||
.description("List agents for a company")
|
||||
.requiredOption("-C, --company-id <id>", "Company ID")
|
||||
.description(`List agents for a ${VOCAB.company.toLowerCase()}`)
|
||||
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.action(async (opts: AgentListOptions) => {
|
||||
try {
|
||||
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
||||
|
|
@ -222,7 +223,7 @@ export function registerAgentCommands(program: Command): void {
|
|||
"Create an agent API key, install local Paperclip skills for Codex/Claude, and print shell exports",
|
||||
)
|
||||
.argument("<agentRef>", "Agent ID or shortname/url-key")
|
||||
.requiredOption("-C, --company-id <id>", "Company ID")
|
||||
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.option("--key-name <name>", "API key label", "local-cli")
|
||||
.option(
|
||||
"--no-install-skills",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Command } from "commander";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import {
|
||||
createApprovalSchema,
|
||||
requestApprovalRevisionSchema,
|
||||
|
|
@ -48,8 +49,8 @@ export function registerApprovalCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
approval
|
||||
.command("list")
|
||||
.description("List approvals for a company")
|
||||
.requiredOption("-C, --company-id <id>", "Company ID")
|
||||
.description(`List approvals for a ${VOCAB.company.toLowerCase()}`)
|
||||
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.option("--status <status>", "Status filter")
|
||||
.action(async (opts: ApprovalListOptions) => {
|
||||
try {
|
||||
|
|
@ -110,7 +111,7 @@ export function registerApprovalCommands(program: Command): void {
|
|||
approval
|
||||
.command("create")
|
||||
.description("Create an approval request")
|
||||
.requiredOption("-C, --company-id <id>", "Company ID")
|
||||
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.requiredOption("--type <type>", "Approval type (hire_agent|approve_ceo_strategy)")
|
||||
.requiredOption("--payload <json>", "Approval payload as JSON object")
|
||||
.option("--requested-by-agent-id <id>", "Requesting agent ID")
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const IMPORT_INCLUDE_OPTIONS: Array<{
|
|||
{ value: "projects", label: "Projects", hint: "projects and workspace metadata" },
|
||||
{ value: "issues", label: "Tasks", hint: "tasks and recurring routines" },
|
||||
{ value: "agents", label: "Agents", hint: "agent records and org structure" },
|
||||
{ value: "skills", label: "Skills", hint: "company skill packages and references" },
|
||||
{ value: "skills", label: "Skills", hint: `${VOCAB.company.toLowerCase()} skill packages and references` }, // [nexus]
|
||||
];
|
||||
|
||||
const IMPORT_PREVIEW_SAMPLE_LIMIT = 6;
|
||||
|
|
@ -1046,7 +1046,7 @@ export function registerCompanyCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
company
|
||||
.command("list")
|
||||
.description("List companies")
|
||||
.description(`List ${VOCAB.companies.toLowerCase()}`) // [nexus]
|
||||
.action(async (opts: CompanyCommandOptions) => {
|
||||
try {
|
||||
const ctx = resolveCommandContext(opts);
|
||||
|
|
@ -1081,8 +1081,8 @@ export function registerCompanyCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
company
|
||||
.command("get")
|
||||
.description("Get one company")
|
||||
.argument("<companyId>", "Company ID")
|
||||
.description(`Get one ${VOCAB.company.toLowerCase()}`) // [nexus]
|
||||
.argument("<companyId>", `${VOCAB.company} ID`) // [nexus]
|
||||
.action(async (companyId: string, opts: CompanyCommandOptions) => {
|
||||
try {
|
||||
const ctx = resolveCommandContext(opts);
|
||||
|
|
@ -1097,8 +1097,8 @@ export function registerCompanyCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
company
|
||||
.command("export")
|
||||
.description("Export a company into a portable markdown package")
|
||||
.argument("<companyId>", "Company ID")
|
||||
.description(`Export a ${VOCAB.company.toLowerCase()} into a portable markdown package`) // [nexus]
|
||||
.argument("<companyId>", `${VOCAB.company} ID`) // [nexus]
|
||||
.requiredOption("--out <path>", "Output directory")
|
||||
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "company,agents")
|
||||
.option("--skills <values>", "Comma-separated skill slugs/keys to export")
|
||||
|
|
@ -1373,8 +1373,8 @@ export function registerCompanyCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
company
|
||||
.command("delete")
|
||||
.description("Delete a company by ID or shortname/prefix (destructive)")
|
||||
.argument("<selector>", "Company ID or issue prefix (for example PAP)")
|
||||
.description(`Delete a ${VOCAB.company.toLowerCase()} by ID or shortname/prefix (destructive)`) // [nexus]
|
||||
.argument("<selector>", `${VOCAB.company} ID or issue prefix (for example PAP)`) // [nexus]
|
||||
.option(
|
||||
"--by <mode>",
|
||||
"Selector mode: auto | id | prefix",
|
||||
|
|
@ -1383,7 +1383,7 @@ export function registerCompanyCommands(program: Command): void {
|
|||
.option("--yes", "Required safety flag to confirm destructive action", false)
|
||||
.option(
|
||||
"--confirm <value>",
|
||||
"Required safety value: target company ID or shortname/prefix",
|
||||
`Required safety value: target ${VOCAB.company.toLowerCase()} ID or shortname/prefix`, // [nexus]
|
||||
)
|
||||
.action(async (selector: string, opts: CompanyDeleteOptions) => {
|
||||
try {
|
||||
|
|
@ -1425,7 +1425,7 @@ export function registerCompanyCommands(program: Command): void {
|
|||
} catch (error) {
|
||||
if (error instanceof ApiRequestError && error.status === 403 && error.message.includes("Board access required")) {
|
||||
throw new Error(
|
||||
"Board access is required to resolve companies across the instance. Use a company ID/prefix for your current company, or run with board authentication.",
|
||||
`${VOCAB.board} access is required to resolve ${VOCAB.companies.toLowerCase()} across the instance. Use a ${VOCAB.company.toLowerCase()} ID/prefix for your current ${VOCAB.company.toLowerCase()}, or run with ${VOCAB.board.toLowerCase()} authentication.`, // [nexus]
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Command } from "commander";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import type { DashboardSummary } from "@paperclipai/shared";
|
||||
import {
|
||||
addCommonClientOptions,
|
||||
|
|
@ -18,8 +19,8 @@ export function registerDashboardCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
dashboard
|
||||
.command("get")
|
||||
.description("Get dashboard summary for a company")
|
||||
.requiredOption("-C, --company-id <id>", "Company ID")
|
||||
.description(`Get dashboard summary for a ${VOCAB.company.toLowerCase()}`)
|
||||
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.action(async (opts: DashboardGetOptions) => {
|
||||
try {
|
||||
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Command } from "commander";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import {
|
||||
addIssueCommentSchema,
|
||||
checkoutIssueSchema,
|
||||
|
|
@ -67,8 +68,8 @@ export function registerIssueCommands(program: Command): void {
|
|||
addCommonClientOptions(
|
||||
issue
|
||||
.command("list")
|
||||
.description("List issues for a company")
|
||||
.option("-C, --company-id <id>", "Company ID")
|
||||
.description(`List issues for a ${VOCAB.company.toLowerCase()}`)
|
||||
.option("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.option("--status <csv>", "Comma-separated statuses")
|
||||
.option("--assignee-agent-id <id>", "Filter by assignee agent ID")
|
||||
.option("--project-id <id>", "Filter by project ID")
|
||||
|
|
@ -136,7 +137,7 @@ export function registerIssueCommands(program: Command): void {
|
|||
issue
|
||||
.command("create")
|
||||
.description("Create an issue")
|
||||
.requiredOption("-C, --company-id <id>", "Company ID")
|
||||
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
|
||||
.requiredOption("--title <title>", "Issue title")
|
||||
.option("--description <text>", "Issue description")
|
||||
.option("--status <status>", "Issue status")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import fs from "node:fs";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import * as p from "@clack/prompts";
|
||||
|
|
@ -78,7 +79,7 @@ export async function runCommand(opts: RunOptions): Promise<void> {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
p.log.step("Starting Paperclip server...");
|
||||
p.log.step(`Starting ${VOCAB.appName} server...`);
|
||||
const startedServer = await importServerEntry();
|
||||
|
||||
if (shouldGenerateBootstrapInviteAfterStart(config)) {
|
||||
|
|
@ -165,13 +166,13 @@ async function importServerEntry(): Promise<StartedServer> {
|
|||
const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@paperclipai/server";
|
||||
if (isModuleNotFoundError(err) && missingServerEntrypoint) {
|
||||
throw new Error(
|
||||
`Could not locate a Paperclip server entrypoint.\n` +
|
||||
`Could not locate a ${VOCAB.appName} server entrypoint.\n` +
|
||||
`Tried: ${devEntry}, @paperclipai/server\n` +
|
||||
`${formatError(err)}`,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Paperclip server failed to start.\n` +
|
||||
`${VOCAB.appName} server failed to start.\n` +
|
||||
`${formatError(err)}`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ import {
|
|||
type PlannedIssueDocumentMerge,
|
||||
type PlannedIssueInsert,
|
||||
} from "./worktree-merge-history-lib.js";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
|
||||
type WorktreeInitOptions = {
|
||||
name?: string;
|
||||
|
|
@ -1538,7 +1539,7 @@ async function resolveMergeCompany(input: {
|
|||
}
|
||||
|
||||
if (shared.length === 0) {
|
||||
throw new Error("Source and target databases do not share a company id. Pass --company explicitly once both sides match.");
|
||||
throw new Error(`Source and target databases do not share a ${VOCAB.company.toLowerCase()} id. Pass --company explicitly once both sides match.`);
|
||||
}
|
||||
|
||||
const options = shared
|
||||
|
|
@ -2644,7 +2645,7 @@ export function registerWorktreeCommands(program: Command): void {
|
|||
.argument("[source]", "Optional source worktree path, directory name, or branch name (back-compat alias for --from)")
|
||||
.option("--from <worktree>", "Source worktree path, directory name, branch name, or current")
|
||||
.option("--to <worktree>", "Target worktree path, directory name, branch name, or current (defaults to current)")
|
||||
.option("--company <id-or-prefix>", "Shared company id or issue prefix inside the chosen source/target instances")
|
||||
.option("--company <id-or-prefix>", `Shared ${VOCAB.company.toLowerCase()} id or issue prefix inside the chosen source/target instances`)
|
||||
.option("--scope <items>", "Comma-separated scopes to import (issues, comments)", "issues,comments")
|
||||
.option("--apply", "Apply the import after previewing the plan", false)
|
||||
.option("--dry", "Preview only and do not import anything", false)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|||
import { resolveServerDevWatchIgnorePaths } from "../src/dev-watch-ignore.ts";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const tsxCliPath = require.resolve("tsx/dist/cli.mjs");
|
||||
const tsxCliPath = require.resolve("tsx/cli"); // [nexus] use exports map subpath, not deep import
|
||||
const serverRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const ignoreArgs = resolveServerDevWatchIgnorePaths(serverRoot).flatMap((ignorePath) => ["--exclude", ignorePath]);
|
||||
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ export async function startServer(): Promise<StartedServer> {
|
|||
|
||||
const LOCAL_BOARD_USER_ID = "local-board";
|
||||
const LOCAL_BOARD_USER_EMAIL = "local@paperclip.local";
|
||||
const LOCAL_BOARD_USER_NAME = "Owner"; // [nexus] was: "Board"
|
||||
const LOCAL_BOARD_USER_NAME = "Owner"; // [nexus] renamed from Board
|
||||
|
||||
async function ensureLocalTrustedBoardPrincipal(db: any): Promise<void> {
|
||||
const now = new Date();
|
||||
|
|
@ -757,7 +757,7 @@ function isMainModule(metaUrl: string): boolean {
|
|||
|
||||
if (isMainModule(import.meta.url)) {
|
||||
void startServer().catch((err) => {
|
||||
logger.error({ err }, "Paperclip server failed to start");
|
||||
logger.error({ err }, "Nexus server failed to start"); // [nexus]
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import type { CompanyPortabilityManifest } from "@paperclipai/shared";
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
ceo: "Project Manager", // [nexus] was: "CEO"
|
||||
ceo: "Project Manager", // [nexus] renamed from CEO
|
||||
cto: "CTO",
|
||||
cmo: "CMO",
|
||||
cfo: "CFO",
|
||||
|
|
@ -96,7 +96,7 @@ export function generateReadme(
|
|||
// What's Inside table
|
||||
lines.push("## What's Inside");
|
||||
lines.push("");
|
||||
lines.push("> This is an [Agent Company](https://agentcompanies.io) package from [Paperclip](https://paperclip.ing)");
|
||||
lines.push("> This is an Agent Workspace package from Nexus"); // [nexus]
|
||||
lines.push("");
|
||||
|
||||
const counts: Array<[string, number]> = [];
|
||||
|
|
@ -157,15 +157,15 @@ export function generateReadme(
|
|||
lines.push("## Getting Started");
|
||||
lines.push("");
|
||||
lines.push("```bash");
|
||||
lines.push("pnpm paperclipai company import this-github-url-or-folder");
|
||||
lines.push("pnpm paperclipai company import this-github-url-or-folder"); // [nexus] CLI command unchanged (code-zone)
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
lines.push("See [Paperclip](https://paperclip.ing) for more information.");
|
||||
lines.push("See the Nexus documentation for more information."); // [nexus]
|
||||
lines.push("");
|
||||
|
||||
// Footer
|
||||
lines.push("---");
|
||||
lines.push(`Exported from [Paperclip](https://paperclip.ing) on ${new Date().toISOString().split("T")[0]}`);
|
||||
lines.push(`Exported from Nexus on ${new Date().toISOString().split("T")[0]}`); // [nexus]
|
||||
lines.push("");
|
||||
|
||||
return lines.join("\n");
|
||||
|
|
|
|||
|
|
@ -2251,7 +2251,7 @@ function buildManifestFromPackageFiles(
|
|||
const companyName =
|
||||
asString(companyFrontmatter.name)
|
||||
?? opts?.sourceLabel?.companyName
|
||||
?? "Imported Company";
|
||||
?? "Imported Workspace";
|
||||
const companySlug =
|
||||
asString(companyFrontmatter.slug)
|
||||
?? normalizeAgentUrlKey(companyName)
|
||||
|
|
@ -3726,7 +3726,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
asString(input.target.newCompanyName) ??
|
||||
sourceManifest.company?.name ??
|
||||
sourceManifest.source?.companyName ??
|
||||
"Imported Company";
|
||||
"Imported Workspace"; // [nexus]
|
||||
const created = await companies.create({
|
||||
name: companyName,
|
||||
description: include.company ? (sourceManifest.company?.description ?? null) : null,
|
||||
|
|
|
|||
|
|
@ -1,142 +1,65 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* E2E: Onboarding wizard flow (skip_llm mode).
|
||||
* E2E: Nexus onboarding wizard — single-step root directory flow.
|
||||
*
|
||||
* Walks through the 4-step OnboardingWizard:
|
||||
* Step 1 — Name your company
|
||||
* Step 2 — Create your first agent (adapter selection + config)
|
||||
* Step 3 — Give it something to do (task creation)
|
||||
* Step 4 — Ready to launch (summary + open issue)
|
||||
*
|
||||
* By default this runs in skip_llm mode: we do NOT assert that an LLM
|
||||
* heartbeat fires. Set PAPERCLIP_E2E_SKIP_LLM=false to enable LLM-dependent
|
||||
* assertions (requires a valid ANTHROPIC_API_KEY).
|
||||
* Verifies:
|
||||
* ONBD-10 — Vite alias intercepts, NexusOnboardingWizard renders
|
||||
* ONBD-11 — Single root-directory input only, no multi-step flow
|
||||
* ONBD-12 — No corporate placeholder text visible
|
||||
*/
|
||||
|
||||
const SKIP_LLM = process.env.PAPERCLIP_E2E_SKIP_LLM !== "false";
|
||||
|
||||
const COMPANY_NAME = `E2E-Test-${Date.now()}`;
|
||||
const AGENT_NAME = "CEO";
|
||||
const TASK_TITLE = "E2E test task";
|
||||
|
||||
test.describe("Onboarding wizard", () => {
|
||||
test("completes full wizard flow", async ({ page }) => {
|
||||
test.describe("Nexus onboarding wizard", () => {
|
||||
test("single-step flow: root dir input, no corporate strings, lands on dashboard", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const wizardHeading = page.locator("h3", { hasText: "Name your company" });
|
||||
const newCompanyBtn = page.getByRole("button", { name: "New Company" });
|
||||
// ONBD-10 + ONBD-11: Nexus wizard renders with single-step heading
|
||||
const heading = page.locator("h1", { hasText: "Welcome to Nexus" });
|
||||
await expect(heading).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await expect(
|
||||
wizardHeading.or(newCompanyBtn)
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
// ONBD-11: Only a root directory input — no multi-step navigation
|
||||
await expect(page.getByRole("button", { name: "Next" })).toHaveCount(0);
|
||||
await expect(page.locator("h3", { hasText: "Name your company" })).toHaveCount(0);
|
||||
await expect(page.locator("h3", { hasText: "Create your first agent" })).toHaveCount(0);
|
||||
await expect(page.locator("h3", { hasText: "Give it something to do" })).toHaveCount(0);
|
||||
await expect(page.locator("h3", { hasText: "Ready to launch" })).toHaveCount(0);
|
||||
|
||||
if (await newCompanyBtn.isVisible()) {
|
||||
await newCompanyBtn.click();
|
||||
}
|
||||
// ONBD-12: No corporate placeholder text
|
||||
await expect(page.getByText("Acme Corp")).toHaveCount(0);
|
||||
await expect(page.getByText("Company name")).toHaveCount(0);
|
||||
await expect(page.getByText("What is this company trying to achieve?")).toHaveCount(0);
|
||||
|
||||
await expect(wizardHeading).toBeVisible({ timeout: 5_000 });
|
||||
// Fill root directory and submit
|
||||
const rootDirInput = page.locator('input[placeholder="~/projects/my-project"]');
|
||||
await expect(rootDirInput).toBeVisible();
|
||||
await rootDirInput.fill("/tmp/nexus-e2e-test");
|
||||
|
||||
const companyNameInput = page.locator('input[placeholder="Acme Corp"]');
|
||||
await companyNameInput.fill(COMPANY_NAME);
|
||||
await page.getByRole("button", { name: "Get Started" }).click();
|
||||
|
||||
const nextButton = page.getByRole("button", { name: "Next" });
|
||||
await nextButton.click();
|
||||
|
||||
await expect(
|
||||
page.locator("h3", { hasText: "Create your first agent" })
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const agentNameInput = page.locator('input[placeholder="CEO"]');
|
||||
await expect(agentNameInput).toHaveValue(AGENT_NAME);
|
||||
|
||||
await expect(
|
||||
page.locator("button", { hasText: "Claude Code" }).locator("..")
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "More Agent Adapter Types" }).click();
|
||||
await expect(page.getByRole("button", { name: "Process" })).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await expect(
|
||||
page.locator("h3", { hasText: "Give it something to do" })
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const taskTitleInput = page.locator(
|
||||
'input[placeholder="e.g. Research competitor pricing"]'
|
||||
);
|
||||
await taskTitleInput.clear();
|
||||
await taskTitleInput.fill(TASK_TITLE);
|
||||
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await expect(
|
||||
page.locator("h3", { hasText: "Ready to launch" })
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await expect(page.locator("text=" + COMPANY_NAME)).toBeVisible();
|
||||
await expect(page.locator("text=" + AGENT_NAME)).toBeVisible();
|
||||
await expect(page.locator("text=" + TASK_TITLE)).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Create & Open Issue" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/issues\//, { timeout: 10_000 });
|
||||
// Should navigate to dashboard
|
||||
await expect(page).toHaveURL(/\/dashboard/, { timeout: 30_000 });
|
||||
|
||||
// Verify workspace and agents created via API
|
||||
const baseUrl = page.url().split("/").slice(0, 3).join("/");
|
||||
|
||||
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
|
||||
expect(companiesRes.ok()).toBe(true);
|
||||
const companies = await companiesRes.json();
|
||||
const company = companies.find(
|
||||
(c: { name: string }) => c.name === COMPANY_NAME
|
||||
);
|
||||
expect(company).toBeTruthy();
|
||||
expect(companies.length).toBeGreaterThan(0);
|
||||
|
||||
const companyId = companies[0].id;
|
||||
const agentsRes = await page.request.get(
|
||||
`${baseUrl}/api/companies/${company.id}/agents`
|
||||
`${baseUrl}/api/companies/${companyId}/agents`
|
||||
);
|
||||
expect(agentsRes.ok()).toBe(true);
|
||||
const agents = await agentsRes.json();
|
||||
const ceoAgent = agents.find(
|
||||
(a: { name: string }) => a.name === AGENT_NAME
|
||||
);
|
||||
expect(ceoAgent).toBeTruthy();
|
||||
expect(ceoAgent.role).toBe("ceo");
|
||||
expect(ceoAgent.adapterType).not.toBe("process");
|
||||
|
||||
const instructionsBundleRes = await page.request.get(
|
||||
`${baseUrl}/api/agents/${ceoAgent.id}/instructions-bundle?companyId=${company.id}`
|
||||
);
|
||||
expect(instructionsBundleRes.ok()).toBe(true);
|
||||
const instructionsBundle = await instructionsBundleRes.json();
|
||||
// PM agent (role: ceo, name: "Project Manager") and Engineer created
|
||||
expect(
|
||||
instructionsBundle.files.map((file: { path: string }) => file.path).sort()
|
||||
).toEqual(["AGENTS.md", "HEARTBEAT.md", "SOUL.md", "TOOLS.md"]);
|
||||
|
||||
const issuesRes = await page.request.get(
|
||||
`${baseUrl}/api/companies/${company.id}/issues`
|
||||
);
|
||||
expect(issuesRes.ok()).toBe(true);
|
||||
const issues = await issuesRes.json();
|
||||
const task = issues.find(
|
||||
(i: { title: string }) => i.title === TASK_TITLE
|
||||
);
|
||||
expect(task).toBeTruthy();
|
||||
expect(task.assigneeAgentId).toBe(ceoAgent.id);
|
||||
expect(task.description).toContain(
|
||||
"You are the CEO. You set the direction for the company."
|
||||
);
|
||||
expect(task.description).not.toContain("github.com/paperclipai/companies");
|
||||
|
||||
if (!SKIP_LLM) {
|
||||
await expect(async () => {
|
||||
const res = await page.request.get(
|
||||
`${baseUrl}/api/issues/${task.id}`
|
||||
);
|
||||
const issue = await res.json();
|
||||
expect(["in_progress", "done"]).toContain(issue.status);
|
||||
}).toPass({ timeout: 120_000, intervals: [5_000] });
|
||||
}
|
||||
agents.some((a: { name: string }) => a.name === "Project Manager")
|
||||
).toBe(true);
|
||||
expect(
|
||||
agents.some((a: { name: string }) => a.name === "Engineer")
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#18181b" />
|
||||
<meta name="theme-color" content="#1e1e2e" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Nexus" />
|
||||
|
|
@ -21,17 +21,18 @@
|
|||
<script>
|
||||
(() => {
|
||||
const key = "paperclip.theme";
|
||||
const darkThemeColor = "#18181b";
|
||||
const lightThemeColor = "#ffffff";
|
||||
const VALID = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
|
||||
try {
|
||||
const stored = window.localStorage.getItem(key);
|
||||
const theme = stored === "light" || stored === "dark" ? stored : "dark";
|
||||
const isDark = theme === "dark";
|
||||
const theme = VALID.includes(stored) ? stored : "catppuccin-mocha";
|
||||
const isDark = theme !== "catppuccin-latte";
|
||||
document.documentElement.classList.toggle("dark", isDark);
|
||||
document.documentElement.classList.toggle("theme-tokyo-night", theme === "tokyo-night");
|
||||
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute("content", isDark ? darkThemeColor : lightThemeColor);
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) {
|
||||
const bg = { "catppuccin-mocha": "#1e1e2e", "tokyo-night": "#1a1b26", "catppuccin-latte": "#eff1f5" };
|
||||
meta.setAttribute("content", bg[theme] || "#1e1e2e");
|
||||
}
|
||||
} catch {
|
||||
document.documentElement.classList.add("dark");
|
||||
|
|
|
|||
|
|
@ -199,13 +199,13 @@ function OnboardingRoutePage() {
|
|||
const title = matchedCompany
|
||||
? `Add another agent to ${matchedCompany.name}`
|
||||
: companies.length > 0
|
||||
? "Create another company"
|
||||
: "Create your first company";
|
||||
? `Create another ${VOCAB.company.toLowerCase()}`
|
||||
: `Create your first ${VOCAB.company.toLowerCase()}`;
|
||||
const description = matchedCompany
|
||||
? "Run onboarding again to add an agent and a starter task for this company."
|
||||
? `Run onboarding again to add an agent and a starter task for this ${VOCAB.company.toLowerCase()}.`
|
||||
: companies.length > 0
|
||||
? "Run onboarding again to create another company and seed its first agent."
|
||||
: "Get started by creating a company and your first agent.";
|
||||
? `Run onboarding again to create another ${VOCAB.company.toLowerCase()} and seed its first agent.`
|
||||
: `Get started by creating a ${VOCAB.company.toLowerCase()} and your first agent.`;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-xl py-10">
|
||||
|
|
@ -287,12 +287,12 @@ function NoCompaniesStartPage() {
|
|||
return (
|
||||
<div className="mx-auto max-w-xl py-10">
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h1 className="text-xl font-semibold">Create your first company</h1>
|
||||
<h1 className="text-xl font-semibold">{`Create your first ${VOCAB.company.toLowerCase()}`}</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Get started by creating a company.
|
||||
{`Get started by creating a ${VOCAB.company.toLowerCase()}.`}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Button onClick={() => openOnboarding()}>New Company</Button>
|
||||
<Button onClick={() => openOnboarding()}>{`New ${VOCAB.company}`}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import type { AdapterConfigFieldsProps } from "../types";
|
||||
import {
|
||||
|
|
@ -134,7 +135,7 @@ export function OpenClawGatewayConfigFields({
|
|||
|
||||
{!isCreate && (
|
||||
<>
|
||||
<Field label="Paperclip API URL override">
|
||||
<Field label={`${VOCAB.appName} API URL override`}>
|
||||
<DraftInput
|
||||
value={
|
||||
eff(
|
||||
|
|
@ -226,7 +227,7 @@ export function OpenClawGatewayConfigFields({
|
|||
|
||||
<Field label="Device auth">
|
||||
<div className="text-xs text-muted-foreground leading-relaxed">
|
||||
Always enabled for gateway agents. Paperclip persists a device key during onboarding so pairing approvals
|
||||
{`Always enabled for gateway agents. ${VOCAB.appName} persists a device key during onboarding so pairing approvals`}
|
||||
remain stable across runs.
|
||||
</div>
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Database, Gauge, ReceiptText } from "lucide-react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
|
||||
const SURFACES = [
|
||||
|
|
@ -34,7 +35,7 @@ export function AccountingModelCard() {
|
|||
Accounting model
|
||||
</CardTitle>
|
||||
<CardDescription className="max-w-2xl text-sm leading-6">
|
||||
Paperclip now separates request-level inference usage from account-level finance events.
|
||||
{`${VOCAB.appName} now separates request-level inference usage from account-level finance events.`}
|
||||
That keeps provider reporting honest when the biller is OpenRouter, Cloudflare, Bedrock, or another intermediary.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared";
|
||||
import type {
|
||||
|
|
@ -186,7 +187,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
|
||||
const createSecret = useMutation({
|
||||
mutationFn: (input: { name: string; value: string }) => {
|
||||
if (!selectedCompanyId) throw new Error("Select a company to create secrets");
|
||||
if (!selectedCompanyId) throw new Error(`Select a ${VOCAB.company.toLowerCase()} to create secrets`);
|
||||
return secretsApi.create(selectedCompanyId, input);
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
@ -197,7 +198,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
|
||||
const uploadMarkdownImage = useMutation({
|
||||
mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => {
|
||||
if (!selectedCompanyId) throw new Error("Select a company to upload images");
|
||||
if (!selectedCompanyId) throw new Error(`Select a ${VOCAB.company.toLowerCase()} to upload images`);
|
||||
return assetsApi.uploadImage(selectedCompanyId, file, namespace);
|
||||
},
|
||||
});
|
||||
|
|
@ -359,7 +360,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
const testEnvironment = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!selectedCompanyId) {
|
||||
throw new Error("Select a company to test adapter environment");
|
||||
throw new Error(`Select a ${VOCAB.company.toLowerCase()} to test adapter environment`);
|
||||
}
|
||||
return agentsApi.testEnvironment(selectedCompanyId, adapterType, {
|
||||
adapterConfig: buildAdapterConfigForTest(),
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ export function IssuesList({
|
|||
<button
|
||||
className={`p-1.5 transition-colors ${viewState.viewMode === "board" ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
||||
onClick={() => updateView({ viewMode: "board" })}
|
||||
title="Board view"
|
||||
title="Kanban view"
|
||||
>
|
||||
<Columns3 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
|
|
@ -494,7 +495,7 @@ const SecretField = React.memo(({
|
|||
label={label}
|
||||
description={
|
||||
description ||
|
||||
"This secret is stored securely via the Paperclip secret provider."
|
||||
`This secret is stored securely via the ${VOCAB.appName} secret provider.`
|
||||
}
|
||||
required={isRequired}
|
||||
error={error}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { useDialog } from "../context/DialogContext";
|
|||
import { usePanel } from "../context/PanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { useTheme } from "../context/ThemeContext";
|
||||
import { useTheme, THEME_META } from "../context/ThemeContext";
|
||||
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
||||
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
|
||||
import { healthApi } from "../api/health";
|
||||
|
|
@ -59,6 +59,12 @@ export function Layout() {
|
|||
setSelectedCompanyId,
|
||||
} = useCompany();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const THEME_CYCLE: Record<string, string> = {
|
||||
"catppuccin-mocha": "Tokyo Night",
|
||||
"tokyo-night": "Catppuccin Latte",
|
||||
"catppuccin-latte": "Catppuccin Mocha",
|
||||
};
|
||||
const nextThemeLabel = THEME_CYCLE[theme] ?? "next theme";
|
||||
const { companyPrefix } = useParams<{ companyPrefix: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
|
@ -67,7 +73,7 @@ export function Layout() {
|
|||
const lastMainScrollTop = useRef(0);
|
||||
const [mobileNavVisible, setMobileNavVisible] = useState(true);
|
||||
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
|
||||
const nextTheme = theme === "dark" ? "light" : "dark";
|
||||
const isDarkTheme = THEME_META[theme].dark;
|
||||
const matchedCompany = useMemo(() => {
|
||||
if (!companyPrefix) return null;
|
||||
const requestedPrefix = companyPrefix.toUpperCase();
|
||||
|
|
@ -331,10 +337,10 @@ export function Layout() {
|
|||
size="icon-sm"
|
||||
className="text-muted-foreground shrink-0"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${nextTheme} mode`}
|
||||
title={`Switch to ${nextTheme} mode`}
|
||||
aria-label={`Switch to ${nextThemeLabel}`}
|
||||
title={`Switch to ${nextThemeLabel}`}
|
||||
>
|
||||
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
{isDarkTheme ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -389,10 +395,10 @@ export function Layout() {
|
|||
size="icon-sm"
|
||||
className="text-muted-foreground shrink-0"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${nextTheme} mode`}
|
||||
title={`Switch to ${nextTheme} mode`}
|
||||
aria-label={`Switch to ${nextThemeLabel}`}
|
||||
title={`Switch to ${nextThemeLabel}`}
|
||||
>
|
||||
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
{isDarkTheme ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { isValidElement, useEffect, useId, useState, type ReactNode } from "reac
|
|||
import Markdown, { type Components } from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { cn } from "../lib/utils";
|
||||
import { useTheme } from "../context/ThemeContext";
|
||||
import { useTheme, THEME_META } from "../context/ThemeContext";
|
||||
import { mentionChipInlineStyle, parseMentionChipHref } from "../lib/mention-chips";
|
||||
|
||||
interface MarkdownBodyProps {
|
||||
|
|
@ -97,7 +97,7 @@ export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownB
|
|||
pre: ({ node: _node, children: preChildren, ...preProps }) => {
|
||||
const mermaidSource = extractMermaidSource(preChildren);
|
||||
if (mermaidSource) {
|
||||
return <MermaidDiagramBlock source={mermaidSource} darkMode={theme === "dark"} />;
|
||||
return <MermaidDiagramBlock source={mermaidSource} darkMode={THEME_META[theme].dark} />;
|
||||
}
|
||||
return <pre {...preProps}>{preChildren}</pre>;
|
||||
},
|
||||
|
|
@ -140,7 +140,7 @@ export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownB
|
|||
<div
|
||||
className={cn(
|
||||
"paperclip-markdown prose prose-sm max-w-none break-words overflow-hidden",
|
||||
theme === "dark" && "prose-invert",
|
||||
THEME_META[theme].dark && "prose-invert",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ export function OnboardingWizard() {
|
|||
): Promise<AdapterEnvironmentTestResult | null> {
|
||||
if (!createdCompanyId) {
|
||||
setAdapterEnvError(
|
||||
"Create or select a company before testing adapter environment."
|
||||
`Create or select a ${VOCAB.company.toLowerCase()} before testing adapter environment.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -407,7 +407,7 @@ export function OnboardingWizard() {
|
|||
|
||||
setStep(2);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to create company");
|
||||
setError(err instanceof Error ? err.message : `Failed to create ${VOCAB.company.toLowerCase()}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -715,7 +715,7 @@ export function OnboardingWizard() {
|
|||
</label>
|
||||
<textarea
|
||||
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[60px]"
|
||||
placeholder="What is this company trying to achieve?"
|
||||
placeholder={`What is this ${VOCAB.company.toLowerCase()} trying to achieve?`}
|
||||
value={companyGoal}
|
||||
onChange={(e) => setCompanyGoal(e.target.value)}
|
||||
/>
|
||||
|
|
@ -1051,7 +1051,7 @@ export function OnboardingWizard() {
|
|||
<p className="text-[11px] text-amber-900/90 leading-relaxed">
|
||||
Claude failed while{" "}
|
||||
<span className="font-mono">ANTHROPIC_API_KEY</span>{" "}
|
||||
is set. You can clear it in this CEO adapter config
|
||||
is set. You can clear it in this {VOCAB.ceo} adapter config
|
||||
and retry the probe.
|
||||
</p>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Link } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { Project } from "@paperclipai/shared";
|
||||
|
|
@ -687,7 +688,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||
{codebase.effectiveLocalFolder}
|
||||
</div>
|
||||
{codebase.origin === "managed_checkout" && (
|
||||
<div className="text-[11px] text-muted-foreground">Paperclip-managed folder.</div>
|
||||
<div className="text-[11px] text-muted-foreground">{`${VOCAB.appName}-managed folder.`}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -719,7 +720,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||
|
||||
{hasAdditionalLegacyWorkspaces && (
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
Additional legacy workspace records exist on this project. Paperclip is using the primary workspace as the codebase view.
|
||||
{`Additional legacy workspace records exist on this project. ${VOCAB.appName} is using the primary workspace as the codebase view.`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
import {
|
||||
Popover,
|
||||
|
|
@ -16,7 +17,7 @@ export function ReportsToPicker({
|
|||
onChange,
|
||||
disabled = false,
|
||||
excludeAgentIds = [],
|
||||
disabledEmptyLabel = "Reports to: N/A (CEO)",
|
||||
disabledEmptyLabel,
|
||||
chooseLabel = "Reports to...",
|
||||
}: {
|
||||
agents: Agent[];
|
||||
|
|
@ -27,6 +28,7 @@ export function ReportsToPicker({
|
|||
disabledEmptyLabel?: string;
|
||||
chooseLabel?: string;
|
||||
}) {
|
||||
const label = disabledEmptyLabel ?? `Reports to: N/A (${VOCAB.ceo})`;
|
||||
const [open, setOpen] = useState(false);
|
||||
const exclude = new Set(excludeAgentIds);
|
||||
const rows = agents.filter(
|
||||
|
|
@ -69,7 +71,7 @@ export function ReportsToPicker({
|
|||
<>
|
||||
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
<span className="min-w-0 truncate">
|
||||
{disabled ? disabledEmptyLabel : chooseLabel}
|
||||
{disabled ? label : chooseLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function Sidebar() {
|
|||
/>
|
||||
)}
|
||||
<span className="flex-1 text-sm font-bold text-foreground truncate pl-1">
|
||||
{selectedCompany?.name ?? "Select company"}
|
||||
{selectedCompany?.name ?? `Select ${VOCAB.company.toLowerCase()}`}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
|
|
@ -33,7 +34,7 @@ export const help: Record<string, string> = {
|
|||
dangerouslySkipPermissions: "Run unattended by auto-approving adapter permission prompts when supported.",
|
||||
dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.",
|
||||
search: "Enable Codex web search capability during runs.",
|
||||
workspaceStrategy: "How Paperclip should realize an execution workspace for this agent. Keep project_primary for normal cwd execution, or use git_worktree for issue-scoped isolated checkouts.",
|
||||
workspaceStrategy: `How ${VOCAB.appName} should realize an execution workspace for this agent. Keep project_primary for normal cwd execution, or use git_worktree for issue-scoped isolated checkouts.`,
|
||||
workspaceBaseRef: "Base git ref used when creating a worktree branch. Leave blank to use the resolved workspace ref or HEAD.",
|
||||
workspaceBranchTemplate: "Template for naming derived branches. Supports {{issue.identifier}}, {{issue.title}}, {{agent.name}}, {{project.id}}, {{workspace.repoRef}}, and {{slug}}.",
|
||||
worktreeParentDir: "Directory where derived worktrees should be created. Absolute, ~-prefixed, and repo-relative paths are supported.",
|
||||
|
|
@ -44,8 +45,8 @@ export const help: Record<string, string> = {
|
|||
args: "Command-line arguments, comma-separated.",
|
||||
extraArgs: "Extra CLI arguments for local adapters, comma-separated.",
|
||||
envVars: "Environment variables injected into the adapter process. Use plain values or secret references.",
|
||||
bootstrapPrompt: "Only sent when Paperclip starts a fresh session. Use this for stable setup guidance that should not be repeated on every heartbeat.",
|
||||
payloadTemplateJson: "Optional JSON merged into remote adapter request payloads before Paperclip adds its standard wake and workspace fields.",
|
||||
bootstrapPrompt: `Only sent when ${VOCAB.appName} starts a fresh session. Use this for stable setup guidance that should not be repeated on every heartbeat.`,
|
||||
payloadTemplateJson: `Optional JSON merged into remote adapter request payloads before ${VOCAB.appName} adds its standard wake and workspace fields.`,
|
||||
webhookUrl: "The URL that receives POST requests when the agent is invoked.",
|
||||
heartbeatInterval: "Run this agent automatically on a timer. Useful for periodic tasks like checking for new work.",
|
||||
intervalSec: "Seconds between automatic heartbeat invocations.",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,16 @@ import {
|
|||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
export type Theme = "catppuccin-mocha" | "tokyo-night" | "catppuccin-latte";
|
||||
|
||||
export const THEME_META: Record<Theme, { label: string; dark: boolean; bg: string; primary: string }> = {
|
||||
"catppuccin-mocha": { label: "Catppuccin Mocha", dark: true, bg: "#1e1e2e", primary: "#89b4fa" },
|
||||
"tokyo-night": { label: "Tokyo Night", dark: true, bg: "#1a1b26", primary: "#7aa2f7" },
|
||||
"catppuccin-latte": { label: "Catppuccin Latte", dark: false, bg: "#eff1f5", primary: "#1e66f5" },
|
||||
};
|
||||
|
||||
const VALID_THEMES: Theme[] = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
|
||||
const DEFAULT_THEME: Theme = "catppuccin-mocha";
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
|
|
@ -17,36 +26,47 @@ interface ThemeContextValue {
|
|||
}
|
||||
|
||||
const THEME_STORAGE_KEY = "paperclip.theme";
|
||||
const DARK_THEME_COLOR = "#18181b";
|
||||
const LIGHT_THEME_COLOR = "#ffffff";
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
|
||||
function resolveThemeFromDocument(): Theme {
|
||||
if (typeof document === "undefined") return "dark";
|
||||
return document.documentElement.classList.contains("dark") ? "dark" : "light";
|
||||
function isValidTheme(value: string | null): value is Theme {
|
||||
return value !== null && VALID_THEMES.includes(value as Theme);
|
||||
}
|
||||
|
||||
function readStoredTheme(): Theme {
|
||||
if (typeof document === "undefined") return DEFAULT_THEME;
|
||||
try {
|
||||
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
return isValidTheme(stored) ? stored : DEFAULT_THEME;
|
||||
} catch {
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
if (typeof document === "undefined") return;
|
||||
const isDark = theme === "dark";
|
||||
const meta = THEME_META[theme];
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle("dark", isDark);
|
||||
root.style.colorScheme = isDark ? "dark" : "light";
|
||||
root.classList.toggle("dark", meta.dark);
|
||||
root.classList.toggle("theme-tokyo-night", theme === "tokyo-night");
|
||||
root.style.colorScheme = meta.dark ? "dark" : "light";
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta instanceof HTMLMetaElement) {
|
||||
themeColorMeta.setAttribute("content", isDark ? DARK_THEME_COLOR : LIGHT_THEME_COLOR);
|
||||
themeColorMeta.setAttribute("content", meta.bg);
|
||||
}
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(() => resolveThemeFromDocument());
|
||||
const [theme, setThemeState] = useState<Theme>(() => readStoredTheme());
|
||||
|
||||
const setTheme = useCallback((nextTheme: Theme) => {
|
||||
setThemeState(nextTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setThemeState((current) => (current === "dark" ? "light" : "dark"));
|
||||
setThemeState((current) => {
|
||||
const idx = VALID_THEMES.indexOf(current);
|
||||
return VALID_THEMES[(idx + 1) % VALID_THEMES.length];
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -54,7 +74,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||
try {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
} catch {
|
||||
// Ignore local storage write failures in restricted environments.
|
||||
// Ignore localStorage write failures in restricted environments.
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
|
|
|
|||
168
ui/src/index.css
168
ui/src/index.css
|
|
@ -45,73 +45,109 @@
|
|||
:root {
|
||||
color-scheme: light;
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--background: #eff1f5;
|
||||
--foreground: #4c4f69;
|
||||
--card: #e6e9ef;
|
||||
--card-foreground: #4c4f69;
|
||||
--popover: #e6e9ef;
|
||||
--popover-foreground: #4c4f69;
|
||||
--primary: #1e66f5;
|
||||
--primary-foreground: #eff1f5;
|
||||
--secondary: #ccd0da;
|
||||
--secondary-foreground: #4c4f69;
|
||||
--muted: #ccd0da;
|
||||
--muted-foreground: #9ca0b0;
|
||||
--accent: #bcc0cc;
|
||||
--accent-foreground: #4c4f69;
|
||||
--destructive: #d20f39;
|
||||
--destructive-foreground: #eff1f5;
|
||||
--border: #ccd0da;
|
||||
--input: #ccd0da;
|
||||
--ring: #1e66f5;
|
||||
--chart-1: #1e66f5;
|
||||
--chart-2: #40a02b;
|
||||
--chart-3: #8839ef;
|
||||
--chart-4: #df8e1d;
|
||||
--chart-5: #d20f39;
|
||||
--sidebar: #e6e9ef;
|
||||
--sidebar-foreground: #4c4f69;
|
||||
--sidebar-primary: #1e66f5;
|
||||
--sidebar-primary-foreground: #eff1f5;
|
||||
--sidebar-accent: #ccd0da;
|
||||
--sidebar-accent-foreground: #4c4f69;
|
||||
--sidebar-border: #ccd0da;
|
||||
--sidebar-ring: #1e66f5;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.637 0.237 25.331);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.145 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
color-scheme: dark;
|
||||
--background: #1e1e2e;
|
||||
--foreground: #cdd6f4;
|
||||
--card: #181825;
|
||||
--card-foreground: #cdd6f4;
|
||||
--popover: #181825;
|
||||
--popover-foreground: #cdd6f4;
|
||||
--primary: #89b4fa;
|
||||
--primary-foreground: #1e1e2e;
|
||||
--secondary: #313244;
|
||||
--secondary-foreground: #cdd6f4;
|
||||
--muted: #313244;
|
||||
--muted-foreground: #6c7086;
|
||||
--accent: #45475a;
|
||||
--accent-foreground: #cdd6f4;
|
||||
--destructive: #f38ba8;
|
||||
--destructive-foreground: #1e1e2e;
|
||||
--border: #313244;
|
||||
--input: #313244;
|
||||
--ring: #89b4fa;
|
||||
--chart-1: #89b4fa;
|
||||
--chart-2: #a6e3a1;
|
||||
--chart-3: #cba6f7;
|
||||
--chart-4: #f9e2af;
|
||||
--chart-5: #f38ba8;
|
||||
--sidebar: #181825;
|
||||
--sidebar-foreground: #cdd6f4;
|
||||
--sidebar-primary: #89b4fa;
|
||||
--sidebar-primary-foreground: #1e1e2e;
|
||||
--sidebar-accent: #313244;
|
||||
--sidebar-accent-foreground: #cdd6f4;
|
||||
--sidebar-border: #313244;
|
||||
--sidebar-ring: #89b4fa;
|
||||
}
|
||||
|
||||
.theme-tokyo-night.dark {
|
||||
--background: #1a1b26;
|
||||
--foreground: #c0caf5;
|
||||
--card: #16161e;
|
||||
--card-foreground: #c0caf5;
|
||||
--popover: #16161e;
|
||||
--popover-foreground: #c0caf5;
|
||||
--primary: #7aa2f7;
|
||||
--primary-foreground: #1a1b26;
|
||||
--secondary: #292e42;
|
||||
--secondary-foreground: #c0caf5;
|
||||
--muted: #292e42;
|
||||
--muted-foreground: #565f89;
|
||||
--accent: #3b4261;
|
||||
--accent-foreground: #c0caf5;
|
||||
--destructive: #f7768e;
|
||||
--destructive-foreground: #1a1b26;
|
||||
--border: #292e42;
|
||||
--input: #292e42;
|
||||
--ring: #7aa2f7;
|
||||
--chart-1: #7aa2f7;
|
||||
--chart-2: #9ece6a;
|
||||
--chart-3: #bb9af7;
|
||||
--chart-4: #e0af68;
|
||||
--chart-5: #f7768e;
|
||||
--sidebar: #16161e;
|
||||
--sidebar-foreground: #c0caf5;
|
||||
--sidebar-primary: #7aa2f7;
|
||||
--sidebar-primary-foreground: #1a1b26;
|
||||
--sidebar-accent: #292e42;
|
||||
--sidebar-accent-foreground: #c0caf5;
|
||||
--sidebar-border: #292e42;
|
||||
--sidebar-ring: #7aa2f7;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
@ -160,10 +196,6 @@
|
|||
}
|
||||
|
||||
/* Dark mode scrollbars */
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark *::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { activityApi } from "../api/activity";
|
||||
import { agentsApi } from "../api/agents";
|
||||
|
|
@ -82,7 +83,7 @@ export function Activity() {
|
|||
}, [issues]);
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={History} message="Select a company to view activity." />;
|
||||
return <EmptyState icon={History} message={`Select a ${VOCAB.company.toLowerCase()} to view activity.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -925,7 +925,7 @@ export function AgentDetail() {
|
|||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||
{isPendingApproval && (
|
||||
<p className="text-sm text-amber-500">
|
||||
This agent is pending board approval and cannot be invoked yet.
|
||||
This agent is pending owner approval and cannot be invoked yet.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
@ -1479,11 +1479,11 @@ function ConfigurationTab({
|
|||
const taskAssignLocked = agent.role === "ceo" || canCreateAgents;
|
||||
const taskAssignHint =
|
||||
taskAssignSource === "ceo_role"
|
||||
? "Enabled automatically for CEO agents."
|
||||
? `Enabled automatically for ${VOCAB.ceo} agents.`
|
||||
: taskAssignSource === "agent_creator"
|
||||
? "Enabled automatically while this agent can create new agents."
|
||||
: taskAssignSource === "explicit_grant"
|
||||
? "Enabled via explicit company permission grant."
|
||||
? `Enabled via explicit ${VOCAB.company.toLowerCase()} permission grant.`
|
||||
: "Disabled unless explicitly granted.";
|
||||
|
||||
return (
|
||||
|
|
@ -1727,7 +1727,7 @@ function PromptsTab({
|
|||
|
||||
const uploadMarkdownImage = useMutation({
|
||||
mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => {
|
||||
if (!selectedCompanyId) throw new Error("Select a company to upload images");
|
||||
if (!selectedCompanyId) throw new Error(`Select a ${VOCAB.company.toLowerCase()} to upload images`);
|
||||
return assetsApi.uploadImage(selectedCompanyId, file, namespace);
|
||||
},
|
||||
});
|
||||
|
|
@ -1927,7 +1927,7 @@ function PromptsTab({
|
|||
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={4}>
|
||||
Managed: Paperclip stores and serves the instructions bundle. External: you provide a path on disk where the instructions live.
|
||||
{`Managed: ${VOCAB.appName} stores and serves the instructions bundle. External: you provide a path on disk where the instructions live.`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -1982,7 +1982,7 @@ function PromptsTab({
|
|||
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={4}>
|
||||
The absolute directory on disk where the instructions bundle lives. In managed mode this is set by Paperclip automatically.
|
||||
{`The absolute directory on disk where the instructions bundle lives. In managed mode this is set by ${VOCAB.appName} automatically.`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -2512,9 +2512,9 @@ function AgentSkillsTab({
|
|||
const unsupportedSkillMessage = useMemo(() => {
|
||||
if (skillSnapshot?.mode !== "unsupported") return null;
|
||||
if (agent.adapterType === "openclaw_gateway") {
|
||||
return "Paperclip cannot manage OpenClaw skills here. Visit your OpenClaw instance to manage this agent's skills.";
|
||||
return `${VOCAB.appName} cannot manage OpenClaw skills here. Visit your OpenClaw instance to manage this agent's skills.`;
|
||||
}
|
||||
return "Paperclip cannot manage skills for this adapter yet. Manage them in the adapter directly.";
|
||||
return `${VOCAB.appName} cannot manage skills for this adapter yet. Manage them in the adapter directly.`;
|
||||
}, [agent.adapterType, skillSnapshot?.mode]);
|
||||
const hasUnsavedChanges = !arraysEqual(skillDraft, lastSavedSkills);
|
||||
const saveStatusLabel = syncSkills.isPending
|
||||
|
|
@ -2672,7 +2672,7 @@ function AgentSkillsTab({
|
|||
<section className="border-y border-border">
|
||||
<div className="border-b border-border bg-muted/40 px-3 py-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Required by Paperclip
|
||||
{`Required by ${VOCAB.appName}`}
|
||||
</span>
|
||||
</div>
|
||||
{requiredSkillRows.map(renderSkillRow)}
|
||||
|
|
@ -2683,7 +2683,7 @@ function AgentSkillsTab({
|
|||
<section className="border-y border-border">
|
||||
<div className="border-b border-border bg-muted/40 px-3 py-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
User-installed skills, not managed by Paperclip
|
||||
{`User-installed skills, not managed by ${VOCAB.appName}`}
|
||||
</span>
|
||||
</div>
|
||||
{unmanagedSkillRows.map(renderSkillRow)}
|
||||
|
|
@ -2695,7 +2695,7 @@ function AgentSkillsTab({
|
|||
|
||||
{desiredOnlyMissingSkills.length > 0 && (
|
||||
<div className="rounded-xl border border-amber-300/60 bg-amber-50/60 px-4 py-3 text-sm text-amber-800 dark:border-amber-500/30 dark:bg-amber-950/20 dark:text-amber-200">
|
||||
<div className="font-medium">Requested skills missing from the company library</div>
|
||||
<div className="font-medium">{`Requested skills missing from the ${VOCAB.company.toLowerCase()} library`}</div>
|
||||
<div className="mt-1 text-xs">
|
||||
{desiredOnlyMissingSkills.join(", ")}
|
||||
</div>
|
||||
|
|
@ -3969,7 +3969,7 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
|
|||
Create API Key
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
API keys allow this agent to authenticate calls to the Paperclip server.
|
||||
{`API keys allow this agent to authenticate calls to the ${VOCAB.appName} server.`}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect, useMemo } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Link, useNavigate, useLocation } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { agentsApi, type OrgNode } from "../api/agents";
|
||||
|
|
@ -122,7 +123,7 @@ export function Agents() {
|
|||
}, [setBreadcrumbs]);
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Bot} message="Select a company to view agents." />;
|
||||
return <EmptyState icon={Bot} message={`Select a ${VOCAB.company.toLowerCase()} to view agents.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useNavigate, useLocation } from "@/lib/router";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { approvalsApi } from "../api/approvals";
|
||||
|
|
@ -75,7 +76,7 @@ export function Approvals() {
|
|||
).length;
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <p className="text-sm text-muted-foreground">Select a company first.</p>;
|
||||
return <p className="text-sm text-muted-foreground">{`Select a ${VOCAB.company.toLowerCase()} first.`}</p>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export function CliAuthPage() {
|
|||
</div>
|
||||
{challenge.requestedCompanyName && (
|
||||
<div>
|
||||
<div className="text-muted-foreground">Requested company</div>
|
||||
<div className="text-muted-foreground">{`Requested ${VOCAB.company.toLowerCase()}`}</div>
|
||||
<div className="text-foreground">{challenge.requestedCompanyName}</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function Companies() {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([{ label: "Companies" }]);
|
||||
setBreadcrumbs([{ label: VOCAB.companies }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
function startEdit(companyId: string, currentName: string) {
|
||||
|
|
@ -98,7 +98,7 @@ export function Companies() {
|
|||
</div>
|
||||
|
||||
<div className="h-6">
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading companies...</p>}
|
||||
{loading && <p className="text-sm text-muted-foreground">{`Loading ${VOCAB.companies.toLowerCase()}...`}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
</div>
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ export function Companies() {
|
|||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<p className="text-sm text-destructive font-medium">
|
||||
Delete this company and all its data? This cannot be undone.
|
||||
{`Delete this ${VOCAB.company.toLowerCase()} and all its data? This cannot be undone.`}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 ml-4 shrink-0">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -912,7 +912,7 @@ export function CompanyExport() {
|
|||
}
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Package} message="Select a company to export." />;
|
||||
return <EmptyState icon={Package} message={`Select a ${VOCAB.company.toLowerCase()} to export.`} />;
|
||||
}
|
||||
|
||||
if (exportPreviewMutation.isPending && !exportData) {
|
||||
|
|
|
|||
|
|
@ -704,7 +704,7 @@ export function CompanyImport() {
|
|||
}, [companyAgents]);
|
||||
|
||||
const localZipHelpText =
|
||||
"Upload a .zip exported directly from Paperclip. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly.";
|
||||
`Upload a .zip exported directly from ${VOCAB.appName}. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly.`;
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([
|
||||
|
|
@ -1086,7 +1086,7 @@ export function CompanyImport() {
|
|||
const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null;
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Download} message="Select a company to import into." />;
|
||||
return <EmptyState icon={Download} message={`Select a ${VOCAB.company.toLowerCase()} to import into.`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -1096,7 +1096,7 @@ export function CompanyImport() {
|
|||
<div>
|
||||
<h2 className="text-base font-semibold">Import source</h2>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Choose a GitHub repo or upload a local Paperclip zip package.
|
||||
{`Choose a GitHub repo or upload a local ${VOCAB.appName} zip package.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -1178,7 +1178,7 @@ export function CompanyImport() {
|
|||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Target" hint="Import into this company or create a new one.">
|
||||
<Field label="Target" hint={`Import into this ${VOCAB.company.toLowerCase()} or create a new one.`}>
|
||||
<select
|
||||
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
|
||||
value={targetMode}
|
||||
|
|
@ -1187,16 +1187,16 @@ export function CompanyImport() {
|
|||
setImportPreview(null);
|
||||
}}
|
||||
>
|
||||
<option value="new">Create new company</option>
|
||||
<option value="new">{`Create new ${VOCAB.company.toLowerCase()}`}</option>
|
||||
<option value="existing">
|
||||
Existing company: {selectedCompany?.name}
|
||||
{`Existing ${VOCAB.company.toLowerCase()}: ${selectedCompany?.name}`}
|
||||
</option>
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
{targetMode === "new" && (
|
||||
<Field
|
||||
label="New company name"
|
||||
label={`New ${VOCAB.company.toLowerCase()} name`}
|
||||
hint="Optional override. Leave blank to use the package name."
|
||||
>
|
||||
<input
|
||||
|
|
@ -1211,7 +1211,7 @@ export function CompanyImport() {
|
|||
|
||||
<Field
|
||||
label="Collision strategy"
|
||||
hint="Board imports can rename, skip, or replace matching company content."
|
||||
hint={`${VOCAB.board} imports can rename, skip, or replace matching ${VOCAB.company.toLowerCase()} content.`}
|
||||
>
|
||||
<select
|
||||
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ export function CompanySettings() {
|
|||
if (!selectedCompany) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No company selected. Select a company from the switcher above.
|
||||
{`No ${VOCAB.company.toLowerCase()} selected. Select a ${VOCAB.company.toLowerCase()} from the switcher above.`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -989,7 +989,7 @@ export function CompanySkills() {
|
|||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Boxes} message="Select a company to manage skills." />;
|
||||
return <EmptyState icon={Boxes} message={`Select a ${VOCAB.company.toLowerCase()} to manage skills.`} />;
|
||||
}
|
||||
|
||||
function handleAddSkillSource() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useRef, useState, type ComponentType } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
BudgetPolicySummary,
|
||||
|
|
@ -35,6 +36,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||
|
||||
const NO_COMPANY = "__none__";
|
||||
|
||||
const SCOPE_LABELS: Record<string, string> = {
|
||||
company: VOCAB.company,
|
||||
agent: "Agent",
|
||||
project: "Project",
|
||||
};
|
||||
|
||||
function currentWeekRange(): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
|
|
@ -529,7 +536,7 @@ export function Costs() {
|
|||
}), [budgetPolicies]);
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={DollarSign} message="Select a company to view costs." />;
|
||||
return <EmptyState icon={DollarSign} message={`Select a ${VOCAB.company.toLowerCase()} to view costs.`} />;
|
||||
}
|
||||
|
||||
const showCustomPrompt = preset === "custom" && !customReady;
|
||||
|
|
@ -855,7 +862,7 @@ export function Costs() {
|
|||
<MetricTile
|
||||
label="Pending approvals"
|
||||
value={String(budgetData?.pendingApprovalCount ?? 0)}
|
||||
subtitle="Budget override approvals awaiting board action"
|
||||
subtitle="Budget override approvals awaiting owner action"
|
||||
icon={ArrowUpRight}
|
||||
/>
|
||||
<MetricTile
|
||||
|
|
@ -907,10 +914,10 @@ export function Costs() {
|
|||
return (
|
||||
<section key={scopeType} className="space-y-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold capitalize">{scopeType} budgets</h2>
|
||||
<h2 className="text-lg font-semibold">{SCOPE_LABELS[scopeType] ?? scopeType} budgets</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{scopeType === "company"
|
||||
? "Company-wide monthly policy."
|
||||
? `${VOCAB.company}-wide monthly policy.`
|
||||
: scopeType === "agent"
|
||||
? "Recurring monthly spend policies for individual agents."
|
||||
: "Lifetime spend policies for execution-bound projects."}
|
||||
|
|
@ -939,7 +946,7 @@ export function Costs() {
|
|||
{budgetPolicies.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="px-5 py-8 text-sm text-muted-foreground">
|
||||
No budget policies yet. Set agent and project budgets from their detail pages, or use the existing company monthly budget control.
|
||||
{`No budget policies yet. Set agent and project budgets from their detail pages, or use the existing ${VOCAB.company.toLowerCase()} monthly budget control.`}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -277,8 +277,8 @@ export function Dashboard() {
|
|||
description={
|
||||
<span>
|
||||
{data.budgets.pendingApprovals > 0
|
||||
? `${data.budgets.pendingApprovals} budget overrides awaiting board review`
|
||||
: "Awaiting board review"}
|
||||
? `${data.budgets.pendingApprovals} budget overrides awaiting owner review`
|
||||
: "Awaiting owner review"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import {
|
||||
BookOpen,
|
||||
Bot,
|
||||
|
|
@ -194,7 +195,7 @@ export function DesignGuide() {
|
|||
<div>
|
||||
<h2 className="text-xl font-bold">Design Guide</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Every component, style, and pattern used across Paperclip.
|
||||
{`Every component, style, and pattern used across ${VOCAB.appName}.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -736,7 +737,7 @@ export function DesignGuide() {
|
|||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Paperclip App</BreadcrumbLink>
|
||||
<BreadcrumbLink href="#">{`${VOCAB.appName} App`}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
|
|
@ -943,7 +944,7 @@ export function DesignGuide() {
|
|||
|
||||
<SubSection title="Initials derivation">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Identity name="CEO Agent" size="sm" />
|
||||
<Identity name={`${VOCAB.ceo} Agent`} size="sm" />
|
||||
<Identity name="Alpha" size="sm" />
|
||||
<Identity name="Quality Assurance Lead" size="sm" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { goalsApi } from "../api/goals";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
|
|
@ -27,7 +28,7 @@ export function Goals() {
|
|||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Target} message="Select a company to view goals." />;
|
||||
return <EmptyState icon={Target} message={`Select a ${VOCAB.company.toLowerCase()} to view goals.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Link, useLocation, useNavigate } from "@/lib/router";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { approvalsApi } from "../api/approvals";
|
||||
|
|
@ -909,7 +910,7 @@ export function Inbox() {
|
|||
};
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />;
|
||||
return <EmptyState icon={InboxIcon} message={`Select a ${VOCAB.company.toLowerCase()} to view inbox.`} />;
|
||||
}
|
||||
|
||||
const hasRunFailures = failedRuns.length > 0;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { SlidersHorizontal } from "lucide-react";
|
||||
import { instanceSettingsApi } from "@/api/instanceSettings";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useTheme, THEME_META, type Theme } from "../context/ThemeContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const ORDERED_THEMES: Theme[] = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
|
||||
|
||||
export function InstanceGeneralSettings() {
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([
|
||||
|
|
@ -69,6 +73,37 @@ export function InstanceGeneralSettings() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<section className="rounded-xl border border-border bg-card p-5">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-sm font-semibold">Theme</h2>
|
||||
<p className="text-sm text-muted-foreground">Choose the visual theme for Nexus.</p>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{ORDERED_THEMES.map((id) => {
|
||||
const meta = THEME_META[id];
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => setTheme(id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-lg border px-3 py-2 text-sm transition-colors",
|
||||
theme === id
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-border bg-card text-foreground hover:border-ring",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="h-4 w-4 rounded-full border border-border/50 shrink-0"
|
||||
style={{ backgroundColor: meta.primary }}
|
||||
/>
|
||||
{meta.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-border bg-card p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1.5">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Clock3, ExternalLink, Settings } from "lucide-react";
|
||||
import type { InstanceSchedulerHeartbeatAgent } from "@paperclipai/shared";
|
||||
|
|
@ -171,14 +172,14 @@ export function InstanceSettings() {
|
|||
<h1 className="text-lg font-semibold">Scheduler Heartbeats</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Agents with a timer heartbeat enabled across all of your companies.
|
||||
{`Agents with a timer heartbeat enabled across all of your ${VOCAB.companies.toLowerCase()}.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span><span className="font-semibold text-foreground">{activeCount}</span> active</span>
|
||||
<span><span className="font-semibold text-foreground">{disabledCount}</span> disabled</span>
|
||||
<span><span className="font-semibold text-foreground">{grouped.length}</span> {grouped.length === 1 ? "company" : "companies"}</span>
|
||||
<span><span className="font-semibold text-foreground">{grouped.length}</span> {grouped.length === 1 ? VOCAB.company.toLowerCase() : VOCAB.companies.toLowerCase()}</span>
|
||||
{anyEnabled && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useCallback } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useLocation, useSearchParams } from "@/lib/router";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { issuesApi } from "../api/issues";
|
||||
|
|
@ -93,7 +94,7 @@ export function Issues() {
|
|||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
|
||||
return <EmptyState icon={CircleDot} message={`Select a ${VOCAB.company.toLowerCase()} to view issues.`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
|
|
@ -27,7 +28,7 @@ export function MyIssues() {
|
|||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={ListTodo} message="Select a company to view your issues." />;
|
||||
return <EmptyState icon={ListTodo} message={`Select a ${VOCAB.company.toLowerCase()} to view your issues.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ export function NewAgent() {
|
|||
{/* Footer */}
|
||||
<div className="border-t border-border px-4 py-3">
|
||||
{isFirstAgent && (
|
||||
<p className="text-xs text-muted-foreground mb-2">This will be the CEO</p>
|
||||
<p className="text-xs text-muted-foreground mb-2">{`This will be the ${VOCAB.ceo}`}</p>
|
||||
)}
|
||||
{formError && (
|
||||
<p className="text-xs text-destructive mb-2">{formError}</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Link } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { agentsApi, type OrgNode } from "../api/agents";
|
||||
|
|
@ -104,7 +105,7 @@ export function Org() {
|
|||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={GitBranch} message="Select a company to view org chart." />;
|
||||
return <EmptyState icon={GitBranch} message={`Select a ${VOCAB.company.toLowerCase()} to view org chart.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Link, useNavigate } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { agentsApi, type OrgNode } from "../api/agents";
|
||||
|
|
@ -256,7 +257,7 @@ export function OrgChart() {
|
|||
}, [zoom, pan]);
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Network} message="Select a company to view the org chart." />;
|
||||
return <EmptyState icon={Network} message={`Select a ${VOCAB.company.toLowerCase()} to view the org chart.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Link, Navigate, useParams } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCompany } from "@/context/CompanyContext";
|
||||
|
|
@ -104,7 +105,7 @@ export function PluginPage() {
|
|||
}
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">Select a company to view this page.</p>
|
||||
<p className="text-sm text-muted-foreground">{`Select a ${VOCAB.company.toLowerCase()} to view this page.`}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
|
|
@ -33,7 +34,7 @@ export function Projects() {
|
|||
);
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Hexagon} message="Select a company to view projects." />;
|
||||
return <EmptyState icon={Hexagon} message={`Select a ${VOCAB.company.toLowerCase()} to view projects.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { VOCAB } from "@paperclipai/branding";
|
||||
import { Link, useLocation, useNavigate, useParams } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
|
|
@ -402,7 +403,7 @@ export function RoutineDetail() {
|
|||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to save routine",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not save the routine.",
|
||||
body: error instanceof Error ? error.message : `${VOCAB.appName} could not save the routine.`,
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
|
|
@ -423,7 +424,7 @@ export function RoutineDetail() {
|
|||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Routine run failed",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not start the routine run.",
|
||||
body: error instanceof Error ? error.message : `${VOCAB.appName} could not start the routine run.`,
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
|
|
@ -445,7 +446,7 @@ export function RoutineDetail() {
|
|||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to update routine",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not update the routine.",
|
||||
body: error instanceof Error ? error.message : `${VOCAB.appName} could not update the routine.`,
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
|
|
@ -486,7 +487,7 @@ export function RoutineDetail() {
|
|||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to add trigger",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not create the trigger.",
|
||||
body: error instanceof Error ? error.message : `${VOCAB.appName} could not create the trigger.`,
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
|
|
@ -504,7 +505,7 @@ export function RoutineDetail() {
|
|||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to update trigger",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not update the trigger.",
|
||||
body: error instanceof Error ? error.message : `${VOCAB.appName} could not update the trigger.`,
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
|
|
@ -522,7 +523,7 @@ export function RoutineDetail() {
|
|||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to delete trigger",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not delete the trigger.",
|
||||
body: error instanceof Error ? error.message : `${VOCAB.appName} could not delete the trigger.`,
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
|
|
@ -544,7 +545,7 @@ export function RoutineDetail() {
|
|||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to rotate webhook secret",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not rotate the webhook secret.",
|
||||
body: error instanceof Error ? error.message : `${VOCAB.appName} could not rotate the webhook secret.`,
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
|
|
@ -584,7 +585,7 @@ export function RoutineDetail() {
|
|||
const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null;
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Repeat} message="Select a company to view routines." />;
|
||||
return <EmptyState icon={Repeat} message={`Select a ${VOCAB.company.toLowerCase()} to view routines.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
@ -673,7 +674,7 @@ export function RoutineDetail() {
|
|||
<div className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-4 space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="font-medium">{secretMessage.title}</p>
|
||||
<p className="text-xs text-muted-foreground">Save this now. Paperclip will not show the secret value again.</p>
|
||||
<p className="text-xs text-muted-foreground">{`Save this now. ${VOCAB.appName} will not show the secret value again.`}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export function Routines() {
|
|||
const currentProject = draft.projectId ? projectById.get(draft.projectId) ?? null : null;
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Repeat} message="Select a company to view routines." />;
|
||||
return <EmptyState icon={Repeat} message={`Select a ${VOCAB.company.toLowerCase()} to view routines.`} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,20 @@ import tailwindcss from "@tailwindcss/vite";
|
|||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
|
||||
// [nexus] Replace upstream OnboardingWizard with Nexus single-step version
|
||||
[path.resolve(__dirname, "src/components/OnboardingWizard")]:
|
||||
path.resolve(__dirname, "./src/components/NexusOnboardingWizard"),
|
||||
},
|
||||
alias: [
|
||||
{ find: "@", replacement: path.resolve(__dirname, "./src") },
|
||||
{
|
||||
find: "lexical",
|
||||
replacement: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
|
||||
},
|
||||
// [nexus] Replace upstream OnboardingWizard with Nexus single-step version.
|
||||
// RegExp required: string keys match the RAW import specifier (not resolved path).
|
||||
// App.tsx imports './components/OnboardingWizard' — must match exactly.
|
||||
{
|
||||
find: /^\.\/components\/OnboardingWizard$/,
|
||||
replacement: path.resolve(__dirname, "./src/components/NexusOnboardingWizard"),
|
||||
},
|
||||
],
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue