Compare commits

..

No commits in common. "v1.1" and "PAP-878-create-a-mine-tab-in-inbox" have entirely different histories.

59 changed files with 351 additions and 414 deletions

View file

@ -4,7 +4,6 @@ import {
readAgentJwtSecretFromEnvFile, readAgentJwtSecretFromEnvFile,
resolveAgentJwtEnvFile, resolveAgentJwtEnvFile,
} from "../config/env.js"; } from "../config/env.js";
import { VOCAB } from "@paperclipai/branding";
import type { CheckResult } from "./index.js"; import type { CheckResult } from "./index.js";
export function agentJwtSecretCheck(configPath?: string): CheckResult { export function agentJwtSecretCheck(configPath?: string): CheckResult {
@ -24,7 +23,7 @@ export function agentJwtSecretCheck(configPath?: string): CheckResult {
name: "Agent JWT secret", name: "Agent JWT secret",
status: "warn", status: "warn",
message: `PAPERCLIP_AGENT_JWT_SECRET is present in ${envPath} but not loaded into environment`, 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 ${VOCAB.appName} server`, repairHint: `Set the value from ${envPath} in your shell before starting the Paperclip server`,
}; };
} }

View file

@ -1,5 +1,4 @@
import type { PaperclipConfig } from "../config/schema.js"; import type { PaperclipConfig } from "../config/schema.js";
import { VOCAB } from "@paperclipai/branding";
import type { CheckResult } from "./index.js"; import type { CheckResult } from "./index.js";
function isLoopbackHost(host: string) { function isLoopbackHost(host: string) {
@ -38,7 +37,7 @@ export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
status: "fail", status: "fail",
message: "authenticated mode requires BETTER_AUTH_SECRET (or PAPERCLIP_AGENT_JWT_SECRET)", message: "authenticated mode requires BETTER_AUTH_SECRET (or PAPERCLIP_AGENT_JWT_SECRET)",
canRepair: false, canRepair: false,
repairHint: `Set BETTER_AUTH_SECRET before starting ${VOCAB.appName}`, repairHint: "Set BETTER_AUTH_SECRET before starting Paperclip",
}; };
} }

View file

@ -1,5 +1,4 @@
import * as p from "@clack/prompts"; import * as p from "@clack/prompts";
import { VOCAB } from "@paperclipai/branding";
import pc from "picocolors"; import pc from "picocolors";
import { normalizeHostnameInput } from "../config/hostnames.js"; import { normalizeHostnameInput } from "../config/hostnames.js";
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js"; import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
@ -28,7 +27,7 @@ export async function addAllowedHostname(host: string, opts: { config?: string }
} else { } else {
p.log.success(`Added allowed hostname: ${pc.cyan(normalized)}`); p.log.success(`Added allowed hostname: ${pc.cyan(normalized)}`);
p.log.message( p.log.message(
pc.dim(`Restart the ${VOCAB.appName} server for this change to take effect.`), pc.dim("Restart the Paperclip server for this change to take effect."),
); );
} }

View file

@ -1,5 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import { VOCAB } from "@paperclipai/branding";
import type { ActivityEvent } from "@paperclipai/shared"; import type { ActivityEvent } from "@paperclipai/shared";
import { import {
addCommonClientOptions, addCommonClientOptions,
@ -23,8 +22,8 @@ export function registerActivityCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
activity activity
.command("list") .command("list")
.description(`List ${VOCAB.company.toLowerCase()} activity log entries`) .description("List company activity log entries")
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`) .requiredOption("-C, --company-id <id>", "Company ID")
.option("--agent-id <id>", "Filter by agent ID") .option("--agent-id <id>", "Filter by agent ID")
.option("--entity-type <type>", "Filter by entity type") .option("--entity-type <type>", "Filter by entity type")
.option("--entity-id <id>", "Filter by entity ID") .option("--entity-id <id>", "Filter by entity ID")

View file

@ -1,5 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import { VOCAB } from "@paperclipai/branding";
import type { Agent } from "@paperclipai/shared"; import type { Agent } from "@paperclipai/shared";
import { import {
removeMaintainerOnlySkillSymlinks, removeMaintainerOnlySkillSymlinks,
@ -163,8 +162,8 @@ export function registerAgentCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
agent agent
.command("list") .command("list")
.description(`List agents for a ${VOCAB.company.toLowerCase()}`) .description("List agents for a company")
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`) .requiredOption("-C, --company-id <id>", "Company ID")
.action(async (opts: AgentListOptions) => { .action(async (opts: AgentListOptions) => {
try { try {
const ctx = resolveCommandContext(opts, { requireCompany: true }); const ctx = resolveCommandContext(opts, { requireCompany: true });
@ -223,7 +222,7 @@ export function registerAgentCommands(program: Command): void {
"Create an agent API key, install local Paperclip skills for Codex/Claude, and print shell exports", "Create an agent API key, install local Paperclip skills for Codex/Claude, and print shell exports",
) )
.argument("<agentRef>", "Agent ID or shortname/url-key") .argument("<agentRef>", "Agent ID or shortname/url-key")
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`) .requiredOption("-C, --company-id <id>", "Company ID")
.option("--key-name <name>", "API key label", "local-cli") .option("--key-name <name>", "API key label", "local-cli")
.option( .option(
"--no-install-skills", "--no-install-skills",

View file

@ -1,5 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import { VOCAB } from "@paperclipai/branding";
import { import {
createApprovalSchema, createApprovalSchema,
requestApprovalRevisionSchema, requestApprovalRevisionSchema,
@ -49,8 +48,8 @@ export function registerApprovalCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
approval approval
.command("list") .command("list")
.description(`List approvals for a ${VOCAB.company.toLowerCase()}`) .description("List approvals for a company")
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`) .requiredOption("-C, --company-id <id>", "Company ID")
.option("--status <status>", "Status filter") .option("--status <status>", "Status filter")
.action(async (opts: ApprovalListOptions) => { .action(async (opts: ApprovalListOptions) => {
try { try {
@ -111,7 +110,7 @@ export function registerApprovalCommands(program: Command): void {
approval approval
.command("create") .command("create")
.description("Create an approval request") .description("Create an approval request")
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`) .requiredOption("-C, --company-id <id>", "Company ID")
.requiredOption("--type <type>", "Approval type (hire_agent|approve_ceo_strategy)") .requiredOption("--type <type>", "Approval type (hire_agent|approve_ceo_strategy)")
.requiredOption("--payload <json>", "Approval payload as JSON object") .requiredOption("--payload <json>", "Approval payload as JSON object")
.option("--requested-by-agent-id <id>", "Requesting agent ID") .option("--requested-by-agent-id <id>", "Requesting agent ID")

View file

@ -83,7 +83,7 @@ const IMPORT_INCLUDE_OPTIONS: Array<{
{ value: "projects", label: "Projects", hint: "projects and workspace metadata" }, { value: "projects", label: "Projects", hint: "projects and workspace metadata" },
{ value: "issues", label: "Tasks", hint: "tasks and recurring routines" }, { value: "issues", label: "Tasks", hint: "tasks and recurring routines" },
{ value: "agents", label: "Agents", hint: "agent records and org structure" }, { value: "agents", label: "Agents", hint: "agent records and org structure" },
{ value: "skills", label: "Skills", hint: `${VOCAB.company.toLowerCase()} skill packages and references` }, // [nexus] { value: "skills", label: "Skills", hint: "company skill packages and references" },
]; ];
const IMPORT_PREVIEW_SAMPLE_LIMIT = 6; const IMPORT_PREVIEW_SAMPLE_LIMIT = 6;
@ -1046,7 +1046,7 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
company company
.command("list") .command("list")
.description(`List ${VOCAB.companies.toLowerCase()}`) // [nexus] .description("List companies")
.action(async (opts: CompanyCommandOptions) => { .action(async (opts: CompanyCommandOptions) => {
try { try {
const ctx = resolveCommandContext(opts); const ctx = resolveCommandContext(opts);
@ -1081,8 +1081,8 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
company company
.command("get") .command("get")
.description(`Get one ${VOCAB.company.toLowerCase()}`) // [nexus] .description("Get one company")
.argument("<companyId>", `${VOCAB.company} ID`) // [nexus] .argument("<companyId>", "Company ID")
.action(async (companyId: string, opts: CompanyCommandOptions) => { .action(async (companyId: string, opts: CompanyCommandOptions) => {
try { try {
const ctx = resolveCommandContext(opts); const ctx = resolveCommandContext(opts);
@ -1097,8 +1097,8 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
company company
.command("export") .command("export")
.description(`Export a ${VOCAB.company.toLowerCase()} into a portable markdown package`) // [nexus] .description("Export a company into a portable markdown package")
.argument("<companyId>", `${VOCAB.company} ID`) // [nexus] .argument("<companyId>", "Company ID")
.requiredOption("--out <path>", "Output directory") .requiredOption("--out <path>", "Output directory")
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "company,agents") .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") .option("--skills <values>", "Comma-separated skill slugs/keys to export")
@ -1373,8 +1373,8 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
company company
.command("delete") .command("delete")
.description(`Delete a ${VOCAB.company.toLowerCase()} by ID or shortname/prefix (destructive)`) // [nexus] .description("Delete a company by ID or shortname/prefix (destructive)")
.argument("<selector>", `${VOCAB.company} ID or issue prefix (for example PAP)`) // [nexus] .argument("<selector>", "Company ID or issue prefix (for example PAP)")
.option( .option(
"--by <mode>", "--by <mode>",
"Selector mode: auto | id | prefix", "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("--yes", "Required safety flag to confirm destructive action", false)
.option( .option(
"--confirm <value>", "--confirm <value>",
`Required safety value: target ${VOCAB.company.toLowerCase()} ID or shortname/prefix`, // [nexus] "Required safety value: target company ID or shortname/prefix",
) )
.action(async (selector: string, opts: CompanyDeleteOptions) => { .action(async (selector: string, opts: CompanyDeleteOptions) => {
try { try {
@ -1425,7 +1425,7 @@ export function registerCompanyCommands(program: Command): void {
} catch (error) { } catch (error) {
if (error instanceof ApiRequestError && error.status === 403 && error.message.includes("Board access required")) { if (error instanceof ApiRequestError && error.status === 403 && error.message.includes("Board access required")) {
throw new Error( throw new Error(
`${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] "Board access is required to resolve companies across the instance. Use a company ID/prefix for your current company, or run with board authentication.",
); );
} }
throw error; throw error;

View file

@ -1,5 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import { VOCAB } from "@paperclipai/branding";
import type { DashboardSummary } from "@paperclipai/shared"; import type { DashboardSummary } from "@paperclipai/shared";
import { import {
addCommonClientOptions, addCommonClientOptions,
@ -19,8 +18,8 @@ export function registerDashboardCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
dashboard dashboard
.command("get") .command("get")
.description(`Get dashboard summary for a ${VOCAB.company.toLowerCase()}`) .description("Get dashboard summary for a company")
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`) .requiredOption("-C, --company-id <id>", "Company ID")
.action(async (opts: DashboardGetOptions) => { .action(async (opts: DashboardGetOptions) => {
try { try {
const ctx = resolveCommandContext(opts, { requireCompany: true }); const ctx = resolveCommandContext(opts, { requireCompany: true });

View file

@ -1,5 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import { VOCAB } from "@paperclipai/branding";
import { import {
addIssueCommentSchema, addIssueCommentSchema,
checkoutIssueSchema, checkoutIssueSchema,
@ -68,8 +67,8 @@ export function registerIssueCommands(program: Command): void {
addCommonClientOptions( addCommonClientOptions(
issue issue
.command("list") .command("list")
.description(`List issues for a ${VOCAB.company.toLowerCase()}`) .description("List issues for a company")
.option("-C, --company-id <id>", `${VOCAB.company} ID`) .option("-C, --company-id <id>", "Company ID")
.option("--status <csv>", "Comma-separated statuses") .option("--status <csv>", "Comma-separated statuses")
.option("--assignee-agent-id <id>", "Filter by assignee agent ID") .option("--assignee-agent-id <id>", "Filter by assignee agent ID")
.option("--project-id <id>", "Filter by project ID") .option("--project-id <id>", "Filter by project ID")
@ -137,7 +136,7 @@ export function registerIssueCommands(program: Command): void {
issue issue
.command("create") .command("create")
.description("Create an issue") .description("Create an issue")
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`) .requiredOption("-C, --company-id <id>", "Company ID")
.requiredOption("--title <title>", "Issue title") .requiredOption("--title <title>", "Issue title")
.option("--description <text>", "Issue description") .option("--description <text>", "Issue description")
.option("--status <status>", "Issue status") .option("--status <status>", "Issue status")

View file

@ -1,5 +1,4 @@
import fs from "node:fs"; import fs from "node:fs";
import { VOCAB } from "@paperclipai/branding";
import path from "node:path"; import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
import * as p from "@clack/prompts"; import * as p from "@clack/prompts";
@ -79,7 +78,7 @@ export async function runCommand(opts: RunOptions): Promise<void> {
process.exit(1); process.exit(1);
} }
p.log.step(`Starting ${VOCAB.appName} server...`); p.log.step("Starting Paperclip server...");
const startedServer = await importServerEntry(); const startedServer = await importServerEntry();
if (shouldGenerateBootstrapInviteAfterStart(config)) { if (shouldGenerateBootstrapInviteAfterStart(config)) {
@ -166,13 +165,13 @@ async function importServerEntry(): Promise<StartedServer> {
const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@paperclipai/server"; const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@paperclipai/server";
if (isModuleNotFoundError(err) && missingServerEntrypoint) { if (isModuleNotFoundError(err) && missingServerEntrypoint) {
throw new Error( throw new Error(
`Could not locate a ${VOCAB.appName} server entrypoint.\n` + `Could not locate a Paperclip server entrypoint.\n` +
`Tried: ${devEntry}, @paperclipai/server\n` + `Tried: ${devEntry}, @paperclipai/server\n` +
`${formatError(err)}`, `${formatError(err)}`,
); );
} }
throw new Error( throw new Error(
`${VOCAB.appName} server failed to start.\n` + `Paperclip server failed to start.\n` +
`${formatError(err)}`, `${formatError(err)}`,
); );
} }

View file

@ -77,7 +77,6 @@ import {
type PlannedIssueDocumentMerge, type PlannedIssueDocumentMerge,
type PlannedIssueInsert, type PlannedIssueInsert,
} from "./worktree-merge-history-lib.js"; } from "./worktree-merge-history-lib.js";
import { VOCAB } from "@paperclipai/branding";
type WorktreeInitOptions = { type WorktreeInitOptions = {
name?: string; name?: string;
@ -1539,7 +1538,7 @@ async function resolveMergeCompany(input: {
} }
if (shared.length === 0) { if (shared.length === 0) {
throw new Error(`Source and target databases do not share a ${VOCAB.company.toLowerCase()} id. Pass --company explicitly once both sides match.`); throw new Error("Source and target databases do not share a company id. Pass --company explicitly once both sides match.");
} }
const options = shared const options = shared
@ -2645,7 +2644,7 @@ export function registerWorktreeCommands(program: Command): void {
.argument("[source]", "Optional source worktree path, directory name, or branch name (back-compat alias for --from)") .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("--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("--to <worktree>", "Target worktree path, directory name, branch name, or current (defaults to current)")
.option("--company <id-or-prefix>", `Shared ${VOCAB.company.toLowerCase()} id or issue prefix inside the chosen source/target instances`) .option("--company <id-or-prefix>", "Shared company id or issue prefix inside the chosen source/target instances")
.option("--scope <items>", "Comma-separated scopes to import (issues, comments)", "issues,comments") .option("--scope <items>", "Comma-separated scopes to import (issues, comments)", "issues,comments")
.option("--apply", "Apply the import after previewing the plan", false) .option("--apply", "Apply the import after previewing the plan", false)
.option("--dry", "Preview only and do not import anything", false) .option("--dry", "Preview only and do not import anything", false)

View file

@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
import { resolveServerDevWatchIgnorePaths } from "../src/dev-watch-ignore.ts"; import { resolveServerDevWatchIgnorePaths } from "../src/dev-watch-ignore.ts";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const tsxCliPath = require.resolve("tsx/cli"); // [nexus] use exports map subpath, not deep import const tsxCliPath = require.resolve("tsx/dist/cli.mjs");
const serverRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const serverRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const ignoreArgs = resolveServerDevWatchIgnorePaths(serverRoot).flatMap((ignorePath) => ["--exclude", ignorePath]); const ignoreArgs = resolveServerDevWatchIgnorePaths(serverRoot).flatMap((ignorePath) => ["--exclude", ignorePath]);

View file

@ -185,7 +185,7 @@ export async function startServer(): Promise<StartedServer> {
const LOCAL_BOARD_USER_ID = "local-board"; const LOCAL_BOARD_USER_ID = "local-board";
const LOCAL_BOARD_USER_EMAIL = "local@paperclip.local"; const LOCAL_BOARD_USER_EMAIL = "local@paperclip.local";
const LOCAL_BOARD_USER_NAME = "Owner"; // [nexus] renamed from Board const LOCAL_BOARD_USER_NAME = "Owner"; // [nexus] was: "Board"
async function ensureLocalTrustedBoardPrincipal(db: any): Promise<void> { async function ensureLocalTrustedBoardPrincipal(db: any): Promise<void> {
const now = new Date(); const now = new Date();
@ -757,7 +757,7 @@ function isMainModule(metaUrl: string): boolean {
if (isMainModule(import.meta.url)) { if (isMainModule(import.meta.url)) {
void startServer().catch((err) => { void startServer().catch((err) => {
logger.error({ err }, "Nexus server failed to start"); // [nexus] logger.error({ err }, "Paperclip server failed to start");
process.exit(1); process.exit(1);
}); });
} }

View file

@ -4,7 +4,7 @@
import type { CompanyPortabilityManifest } from "@paperclipai/shared"; import type { CompanyPortabilityManifest } from "@paperclipai/shared";
const ROLE_LABELS: Record<string, string> = { const ROLE_LABELS: Record<string, string> = {
ceo: "Project Manager", // [nexus] renamed from CEO ceo: "Project Manager", // [nexus] was: "CEO"
cto: "CTO", cto: "CTO",
cmo: "CMO", cmo: "CMO",
cfo: "CFO", cfo: "CFO",
@ -96,7 +96,7 @@ export function generateReadme(
// What's Inside table // What's Inside table
lines.push("## What's Inside"); lines.push("## What's Inside");
lines.push(""); lines.push("");
lines.push("> This is an Agent Workspace package from Nexus"); // [nexus] lines.push("> This is an [Agent Company](https://agentcompanies.io) package from [Paperclip](https://paperclip.ing)");
lines.push(""); lines.push("");
const counts: Array<[string, number]> = []; const counts: Array<[string, number]> = [];
@ -157,15 +157,15 @@ export function generateReadme(
lines.push("## Getting Started"); lines.push("## Getting Started");
lines.push(""); lines.push("");
lines.push("```bash"); lines.push("```bash");
lines.push("pnpm paperclipai company import this-github-url-or-folder"); // [nexus] CLI command unchanged (code-zone) lines.push("pnpm paperclipai company import this-github-url-or-folder");
lines.push("```"); lines.push("```");
lines.push(""); lines.push("");
lines.push("See the Nexus documentation for more information."); // [nexus] lines.push("See [Paperclip](https://paperclip.ing) for more information.");
lines.push(""); lines.push("");
// Footer // Footer
lines.push("---"); lines.push("---");
lines.push(`Exported from Nexus on ${new Date().toISOString().split("T")[0]}`); // [nexus] lines.push(`Exported from [Paperclip](https://paperclip.ing) on ${new Date().toISOString().split("T")[0]}`);
lines.push(""); lines.push("");
return lines.join("\n"); return lines.join("\n");

View file

@ -2251,7 +2251,7 @@ function buildManifestFromPackageFiles(
const companyName = const companyName =
asString(companyFrontmatter.name) asString(companyFrontmatter.name)
?? opts?.sourceLabel?.companyName ?? opts?.sourceLabel?.companyName
?? "Imported Workspace"; ?? "Imported Company";
const companySlug = const companySlug =
asString(companyFrontmatter.slug) asString(companyFrontmatter.slug)
?? normalizeAgentUrlKey(companyName) ?? normalizeAgentUrlKey(companyName)
@ -3726,7 +3726,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
asString(input.target.newCompanyName) ?? asString(input.target.newCompanyName) ??
sourceManifest.company?.name ?? sourceManifest.company?.name ??
sourceManifest.source?.companyName ?? sourceManifest.source?.companyName ??
"Imported Workspace"; // [nexus] "Imported Company";
const created = await companies.create({ const created = await companies.create({
name: companyName, name: companyName,
description: include.company ? (sourceManifest.company?.description ?? null) : null, description: include.company ? (sourceManifest.company?.description ?? null) : null,

View file

@ -1,65 +1,142 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
/** /**
* E2E: Nexus onboarding wizard single-step root directory flow. * E2E: Onboarding wizard flow (skip_llm mode).
* *
* Verifies: * Walks through the 4-step OnboardingWizard:
* ONBD-10 Vite alias intercepts, NexusOnboardingWizard renders * Step 1 Name your company
* ONBD-11 Single root-directory input only, no multi-step flow * Step 2 Create your first agent (adapter selection + config)
* ONBD-12 No corporate placeholder text visible * 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).
*/ */
test.describe("Nexus onboarding wizard", () => { const SKIP_LLM = process.env.PAPERCLIP_E2E_SKIP_LLM !== "false";
test("single-step flow: root dir input, no corporate strings, lands on dashboard", async ({ page }) => {
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 }) => {
await page.goto("/"); await page.goto("/");
// ONBD-10 + ONBD-11: Nexus wizard renders with single-step heading const wizardHeading = page.locator("h3", { hasText: "Name your company" });
const heading = page.locator("h1", { hasText: "Welcome to Nexus" }); const newCompanyBtn = page.getByRole("button", { name: "New Company" });
await expect(heading).toBeVisible({ timeout: 15_000 });
// ONBD-11: Only a root directory input — no multi-step navigation await expect(
await expect(page.getByRole("button", { name: "Next" })).toHaveCount(0); wizardHeading.or(newCompanyBtn)
await expect(page.locator("h3", { hasText: "Name your company" })).toHaveCount(0); ).toBeVisible({ timeout: 15_000 });
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);
// ONBD-12: No corporate placeholder text if (await newCompanyBtn.isVisible()) {
await expect(page.getByText("Acme Corp")).toHaveCount(0); await newCompanyBtn.click();
await expect(page.getByText("Company name")).toHaveCount(0); }
await expect(page.getByText("What is this company trying to achieve?")).toHaveCount(0);
// Fill root directory and submit await expect(wizardHeading).toBeVisible({ timeout: 5_000 });
const rootDirInput = page.locator('input[placeholder="~/projects/my-project"]');
await expect(rootDirInput).toBeVisible();
await rootDirInput.fill("/tmp/nexus-e2e-test");
await page.getByRole("button", { name: "Get Started" }).click(); const companyNameInput = page.locator('input[placeholder="Acme Corp"]');
await companyNameInput.fill(COMPANY_NAME);
// Should navigate to dashboard const nextButton = page.getByRole("button", { name: "Next" });
await expect(page).toHaveURL(/\/dashboard/, { timeout: 30_000 }); 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 });
// Verify workspace and agents created via API
const baseUrl = page.url().split("/").slice(0, 3).join("/"); const baseUrl = page.url().split("/").slice(0, 3).join("/");
const companiesRes = await page.request.get(`${baseUrl}/api/companies`); const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
expect(companiesRes.ok()).toBe(true); expect(companiesRes.ok()).toBe(true);
const companies = await companiesRes.json(); const companies = await companiesRes.json();
expect(companies.length).toBeGreaterThan(0); const company = companies.find(
(c: { name: string }) => c.name === COMPANY_NAME
);
expect(company).toBeTruthy();
const companyId = companies[0].id;
const agentsRes = await page.request.get( const agentsRes = await page.request.get(
`${baseUrl}/api/companies/${companyId}/agents` `${baseUrl}/api/companies/${company.id}/agents`
); );
expect(agentsRes.ok()).toBe(true); expect(agentsRes.ok()).toBe(true);
const agents = await agentsRes.json(); 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");
// PM agent (role: ceo, name: "Project Manager") and Engineer created 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();
expect( expect(
agents.some((a: { name: string }) => a.name === "Project Manager") instructionsBundle.files.map((file: { path: string }) => file.path).sort()
).toBe(true); ).toEqual(["AGENTS.md", "HEARTBEAT.md", "SOUL.md", "TOOLS.md"]);
expect(
agents.some((a: { name: string }) => a.name === "Engineer") const issuesRes = await page.request.get(
).toBe(true); `${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] });
}
}); });
}); });

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <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="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#1e1e2e" /> <meta name="theme-color" content="#18181b" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <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-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Nexus" /> <meta name="apple-mobile-web-app-title" content="Nexus" />
@ -21,18 +21,17 @@
<script> <script>
(() => { (() => {
const key = "paperclip.theme"; const key = "paperclip.theme";
const VALID = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"]; const darkThemeColor = "#18181b";
const lightThemeColor = "#ffffff";
try { try {
const stored = window.localStorage.getItem(key); const stored = window.localStorage.getItem(key);
const theme = VALID.includes(stored) ? stored : "catppuccin-mocha"; const theme = stored === "light" || stored === "dark" ? stored : "dark";
const isDark = theme !== "catppuccin-latte"; const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark); document.documentElement.classList.toggle("dark", isDark);
document.documentElement.classList.toggle("theme-tokyo-night", theme === "tokyo-night");
document.documentElement.style.colorScheme = isDark ? "dark" : "light"; document.documentElement.style.colorScheme = isDark ? "dark" : "light";
const meta = document.querySelector('meta[name="theme-color"]'); const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (meta) { if (themeColorMeta) {
const bg = { "catppuccin-mocha": "#1e1e2e", "tokyo-night": "#1a1b26", "catppuccin-latte": "#eff1f5" }; themeColorMeta.setAttribute("content", isDark ? darkThemeColor : lightThemeColor);
meta.setAttribute("content", bg[theme] || "#1e1e2e");
} }
} catch { } catch {
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");

View file

@ -199,13 +199,13 @@ function OnboardingRoutePage() {
const title = matchedCompany const title = matchedCompany
? `Add another agent to ${matchedCompany.name}` ? `Add another agent to ${matchedCompany.name}`
: companies.length > 0 : companies.length > 0
? `Create another ${VOCAB.company.toLowerCase()}` ? "Create another company"
: `Create your first ${VOCAB.company.toLowerCase()}`; : "Create your first company";
const description = matchedCompany const description = matchedCompany
? `Run onboarding again to add an agent and a starter task for this ${VOCAB.company.toLowerCase()}.` ? "Run onboarding again to add an agent and a starter task for this company."
: companies.length > 0 : companies.length > 0
? `Run onboarding again to create another ${VOCAB.company.toLowerCase()} and seed its first agent.` ? "Run onboarding again to create another company and seed its first agent."
: `Get started by creating a ${VOCAB.company.toLowerCase()} and your first agent.`; : "Get started by creating a company and your first agent.";
return ( return (
<div className="mx-auto max-w-xl py-10"> <div className="mx-auto max-w-xl py-10">
@ -287,12 +287,12 @@ function NoCompaniesStartPage() {
return ( return (
<div className="mx-auto max-w-xl py-10"> <div className="mx-auto max-w-xl py-10">
<div className="rounded-lg border border-border bg-card p-6"> <div className="rounded-lg border border-border bg-card p-6">
<h1 className="text-xl font-semibold">{`Create your first ${VOCAB.company.toLowerCase()}`}</h1> <h1 className="text-xl font-semibold">Create your first company</h1>
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
{`Get started by creating a ${VOCAB.company.toLowerCase()}.`} Get started by creating a company.
</p> </p>
<div className="mt-4"> <div className="mt-4">
<Button onClick={() => openOnboarding()}>{`New ${VOCAB.company}`}</Button> <Button onClick={() => openOnboarding()}>New Company</Button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import type { AdapterConfigFieldsProps } from "../types"; import type { AdapterConfigFieldsProps } from "../types";
import { import {
@ -135,7 +134,7 @@ export function OpenClawGatewayConfigFields({
{!isCreate && ( {!isCreate && (
<> <>
<Field label={`${VOCAB.appName} API URL override`}> <Field label="Paperclip API URL override">
<DraftInput <DraftInput
value={ value={
eff( eff(
@ -227,7 +226,7 @@ export function OpenClawGatewayConfigFields({
<Field label="Device auth"> <Field label="Device auth">
<div className="text-xs text-muted-foreground leading-relaxed"> <div className="text-xs text-muted-foreground leading-relaxed">
{`Always enabled for gateway agents. ${VOCAB.appName} persists a device key during onboarding so pairing approvals`} Always enabled for gateway agents. Paperclip persists a device key during onboarding so pairing approvals
remain stable across runs. remain stable across runs.
</div> </div>
</Field> </Field>

View file

@ -1,5 +1,4 @@
import { Database, Gauge, ReceiptText } from "lucide-react"; import { Database, Gauge, ReceiptText } from "lucide-react";
import { VOCAB } from "@paperclipai/branding";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
const SURFACES = [ const SURFACES = [
@ -35,7 +34,7 @@ export function AccountingModelCard() {
Accounting model Accounting model
</CardTitle> </CardTitle>
<CardDescription className="max-w-2xl text-sm leading-6"> <CardDescription className="max-w-2xl text-sm leading-6">
{`${VOCAB.appName} now separates request-level inference usage from account-level finance events.`} Paperclip 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. That keeps provider reporting honest when the biller is OpenRouter, Cloudflare, Bedrock, or another intermediary.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared"; import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared";
import type { import type {
@ -187,7 +186,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const createSecret = useMutation({ const createSecret = useMutation({
mutationFn: (input: { name: string; value: string }) => { mutationFn: (input: { name: string; value: string }) => {
if (!selectedCompanyId) throw new Error(`Select a ${VOCAB.company.toLowerCase()} to create secrets`); if (!selectedCompanyId) throw new Error("Select a company to create secrets");
return secretsApi.create(selectedCompanyId, input); return secretsApi.create(selectedCompanyId, input);
}, },
onSuccess: () => { onSuccess: () => {
@ -198,7 +197,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const uploadMarkdownImage = useMutation({ const uploadMarkdownImage = useMutation({
mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => { mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => {
if (!selectedCompanyId) throw new Error(`Select a ${VOCAB.company.toLowerCase()} to upload images`); if (!selectedCompanyId) throw new Error("Select a company to upload images");
return assetsApi.uploadImage(selectedCompanyId, file, namespace); return assetsApi.uploadImage(selectedCompanyId, file, namespace);
}, },
}); });
@ -360,7 +359,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const testEnvironment = useMutation({ const testEnvironment = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!selectedCompanyId) { if (!selectedCompanyId) {
throw new Error(`Select a ${VOCAB.company.toLowerCase()} to test adapter environment`); throw new Error("Select a company to test adapter environment");
} }
return agentsApi.testEnvironment(selectedCompanyId, adapterType, { return agentsApi.testEnvironment(selectedCompanyId, adapterType, {
adapterConfig: buildAdapterConfigForTest(), adapterConfig: buildAdapterConfigForTest(),

View file

@ -391,7 +391,7 @@ export function IssuesList({
<button <button
className={`p-1.5 transition-colors ${viewState.viewMode === "board" ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"}`} className={`p-1.5 transition-colors ${viewState.viewMode === "board" ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"}`}
onClick={() => updateView({ viewMode: "board" })} onClick={() => updateView({ viewMode: "board" })}
title="Kanban view" title="Board view"
> >
<Columns3 className="h-3.5 w-3.5" /> <Columns3 className="h-3.5 w-3.5" />
</button> </button>

View file

@ -1,5 +1,4 @@
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
@ -495,7 +494,7 @@ const SecretField = React.memo(({
label={label} label={label}
description={ description={
description || description ||
`This secret is stored securely via the ${VOCAB.appName} secret provider.` "This secret is stored securely via the Paperclip secret provider."
} }
required={isRequired} required={isRequired}
error={error} error={error}

View file

@ -20,7 +20,7 @@ import { useDialog } from "../context/DialogContext";
import { usePanel } from "../context/PanelContext"; import { usePanel } from "../context/PanelContext";
import { useCompany } from "../context/CompanyContext"; import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext"; import { useSidebar } from "../context/SidebarContext";
import { useTheme, THEME_META } from "../context/ThemeContext"; import { useTheme } from "../context/ThemeContext";
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts"; import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory"; import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
import { healthApi } from "../api/health"; import { healthApi } from "../api/health";
@ -59,12 +59,6 @@ export function Layout() {
setSelectedCompanyId, setSelectedCompanyId,
} = useCompany(); } = useCompany();
const { theme, toggleTheme } = useTheme(); 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 { companyPrefix } = useParams<{ companyPrefix: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -73,7 +67,7 @@ export function Layout() {
const lastMainScrollTop = useRef(0); const lastMainScrollTop = useRef(0);
const [mobileNavVisible, setMobileNavVisible] = useState(true); const [mobileNavVisible, setMobileNavVisible] = useState(true);
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath()); const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
const isDarkTheme = THEME_META[theme].dark; const nextTheme = theme === "dark" ? "light" : "dark";
const matchedCompany = useMemo(() => { const matchedCompany = useMemo(() => {
if (!companyPrefix) return null; if (!companyPrefix) return null;
const requestedPrefix = companyPrefix.toUpperCase(); const requestedPrefix = companyPrefix.toUpperCase();
@ -337,10 +331,10 @@ export function Layout() {
size="icon-sm" size="icon-sm"
className="text-muted-foreground shrink-0" className="text-muted-foreground shrink-0"
onClick={toggleTheme} onClick={toggleTheme}
aria-label={`Switch to ${nextThemeLabel}`} aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextThemeLabel}`} title={`Switch to ${nextTheme} mode`}
> >
{isDarkTheme ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} {theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button> </Button>
</div> </div>
</div> </div>
@ -395,10 +389,10 @@ export function Layout() {
size="icon-sm" size="icon-sm"
className="text-muted-foreground shrink-0" className="text-muted-foreground shrink-0"
onClick={toggleTheme} onClick={toggleTheme}
aria-label={`Switch to ${nextThemeLabel}`} aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextThemeLabel}`} title={`Switch to ${nextTheme} mode`}
> >
{isDarkTheme ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} {theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -2,7 +2,7 @@ import { isValidElement, useEffect, useId, useState, type ReactNode } from "reac
import Markdown, { type Components } from "react-markdown"; import Markdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useTheme, THEME_META } from "../context/ThemeContext"; import { useTheme } from "../context/ThemeContext";
import { mentionChipInlineStyle, parseMentionChipHref } from "../lib/mention-chips"; import { mentionChipInlineStyle, parseMentionChipHref } from "../lib/mention-chips";
interface MarkdownBodyProps { interface MarkdownBodyProps {
@ -97,7 +97,7 @@ export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownB
pre: ({ node: _node, children: preChildren, ...preProps }) => { pre: ({ node: _node, children: preChildren, ...preProps }) => {
const mermaidSource = extractMermaidSource(preChildren); const mermaidSource = extractMermaidSource(preChildren);
if (mermaidSource) { if (mermaidSource) {
return <MermaidDiagramBlock source={mermaidSource} darkMode={THEME_META[theme].dark} />; return <MermaidDiagramBlock source={mermaidSource} darkMode={theme === "dark"} />;
} }
return <pre {...preProps}>{preChildren}</pre>; return <pre {...preProps}>{preChildren}</pre>;
}, },
@ -140,7 +140,7 @@ export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownB
<div <div
className={cn( className={cn(
"paperclip-markdown prose prose-sm max-w-none break-words overflow-hidden", "paperclip-markdown prose prose-sm max-w-none break-words overflow-hidden",
THEME_META[theme].dark && "prose-invert", theme === "dark" && "prose-invert",
className, className,
)} )}
> >

View file

@ -351,7 +351,7 @@ export function OnboardingWizard() {
): Promise<AdapterEnvironmentTestResult | null> { ): Promise<AdapterEnvironmentTestResult | null> {
if (!createdCompanyId) { if (!createdCompanyId) {
setAdapterEnvError( setAdapterEnvError(
`Create or select a ${VOCAB.company.toLowerCase()} before testing adapter environment.` "Create or select a company before testing adapter environment."
); );
return null; return null;
} }
@ -407,7 +407,7 @@ export function OnboardingWizard() {
setStep(2); setStep(2);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : `Failed to create ${VOCAB.company.toLowerCase()}`); setError(err instanceof Error ? err.message : "Failed to create company");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -715,7 +715,7 @@ export function OnboardingWizard() {
</label> </label>
<textarea <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]" 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 ${VOCAB.company.toLowerCase()} trying to achieve?`} placeholder="What is this company trying to achieve?"
value={companyGoal} value={companyGoal}
onChange={(e) => setCompanyGoal(e.target.value)} onChange={(e) => setCompanyGoal(e.target.value)}
/> />
@ -1051,7 +1051,7 @@ export function OnboardingWizard() {
<p className="text-[11px] text-amber-900/90 leading-relaxed"> <p className="text-[11px] text-amber-900/90 leading-relaxed">
Claude failed while{" "} Claude failed while{" "}
<span className="font-mono">ANTHROPIC_API_KEY</span>{" "} <span className="font-mono">ANTHROPIC_API_KEY</span>{" "}
is set. You can clear it in this {VOCAB.ceo} adapter config is set. You can clear it in this CEO adapter config
and retry the probe. and retry the probe.
</p> </p>
<Button <Button

View file

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link } from "@/lib/router"; import { Link } from "@/lib/router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { Project } from "@paperclipai/shared"; import type { Project } from "@paperclipai/shared";
@ -688,7 +687,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
{codebase.effectiveLocalFolder} {codebase.effectiveLocalFolder}
</div> </div>
{codebase.origin === "managed_checkout" && ( {codebase.origin === "managed_checkout" && (
<div className="text-[11px] text-muted-foreground">{`${VOCAB.appName}-managed folder.`}</div> <div className="text-[11px] text-muted-foreground">Paperclip-managed folder.</div>
)} )}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -720,7 +719,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
{hasAdditionalLegacyWorkspaces && ( {hasAdditionalLegacyWorkspaces && (
<div className="text-[11px] text-muted-foreground"> <div className="text-[11px] text-muted-foreground">
{`Additional legacy workspace records exist on this project. ${VOCAB.appName} is using the primary workspace as the codebase view.`} Additional legacy workspace records exist on this project. Paperclip is using the primary workspace as the codebase view.
</div> </div>
)} )}

View file

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import type { Agent } from "@paperclipai/shared"; import type { Agent } from "@paperclipai/shared";
import { import {
Popover, Popover,
@ -17,7 +16,7 @@ export function ReportsToPicker({
onChange, onChange,
disabled = false, disabled = false,
excludeAgentIds = [], excludeAgentIds = [],
disabledEmptyLabel, disabledEmptyLabel = "Reports to: N/A (CEO)",
chooseLabel = "Reports to...", chooseLabel = "Reports to...",
}: { }: {
agents: Agent[]; agents: Agent[];
@ -28,7 +27,6 @@ export function ReportsToPicker({
disabledEmptyLabel?: string; disabledEmptyLabel?: string;
chooseLabel?: string; chooseLabel?: string;
}) { }) {
const label = disabledEmptyLabel ?? `Reports to: N/A (${VOCAB.ceo})`;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const exclude = new Set(excludeAgentIds); const exclude = new Set(excludeAgentIds);
const rows = agents.filter( const rows = agents.filter(
@ -71,7 +69,7 @@ export function ReportsToPicker({
<> <>
<User className="h-3 w-3 shrink-0 text-muted-foreground" /> <User className="h-3 w-3 shrink-0 text-muted-foreground" />
<span className="min-w-0 truncate"> <span className="min-w-0 truncate">
{disabled ? label : chooseLabel} {disabled ? disabledEmptyLabel : chooseLabel}
</span> </span>
</> </>
)} )}

View file

@ -58,7 +58,7 @@ export function Sidebar() {
/> />
)} )}
<span className="flex-1 text-sm font-bold text-foreground truncate pl-1"> <span className="flex-1 text-sm font-bold text-foreground truncate pl-1">
{selectedCompany?.name ?? `Select ${VOCAB.company.toLowerCase()}`} {selectedCompany?.name ?? "Select company"}
</span> </span>
<Button <Button
variant="ghost" variant="ghost"

View file

@ -1,5 +1,4 @@
import { useState, useRef, useEffect, useCallback } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import { VOCAB } from "@paperclipai/branding";
import { import {
Tooltip, Tooltip,
TooltipTrigger, TooltipTrigger,
@ -34,7 +33,7 @@ export const help: Record<string, string> = {
dangerouslySkipPermissions: "Run unattended by auto-approving adapter permission prompts when supported.", dangerouslySkipPermissions: "Run unattended by auto-approving adapter permission prompts when supported.",
dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.", dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.",
search: "Enable Codex web search capability during runs.", search: "Enable Codex web search capability during runs.",
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.`, 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.",
workspaceBaseRef: "Base git ref used when creating a worktree branch. Leave blank to use the resolved workspace ref or HEAD.", 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}}.", 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.", worktreeParentDir: "Directory where derived worktrees should be created. Absolute, ~-prefixed, and repo-relative paths are supported.",
@ -45,8 +44,8 @@ export const help: Record<string, string> = {
args: "Command-line arguments, comma-separated.", args: "Command-line arguments, comma-separated.",
extraArgs: "Extra CLI arguments for local adapters, 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.", envVars: "Environment variables injected into the adapter process. Use plain values or secret references.",
bootstrapPrompt: `Only sent when ${VOCAB.appName} starts a fresh session. Use this for stable setup guidance that should not be repeated on every heartbeat.`, 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 ${VOCAB.appName} adds its standard wake and workspace fields.`, payloadTemplateJson: "Optional JSON merged into remote adapter request payloads before Paperclip adds its standard wake and workspace fields.",
webhookUrl: "The URL that receives POST requests when the agent is invoked.", 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.", heartbeatInterval: "Run this agent automatically on a timer. Useful for periodic tasks like checking for new work.",
intervalSec: "Seconds between automatic heartbeat invocations.", intervalSec: "Seconds between automatic heartbeat invocations.",

View file

@ -8,16 +8,7 @@ import {
type ReactNode, type ReactNode,
} from "react"; } from "react";
export type Theme = "catppuccin-mocha" | "tokyo-night" | "catppuccin-latte"; type Theme = "light" | "dark";
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 { interface ThemeContextValue {
theme: Theme; theme: Theme;
@ -26,47 +17,36 @@ interface ThemeContextValue {
} }
const THEME_STORAGE_KEY = "paperclip.theme"; const THEME_STORAGE_KEY = "paperclip.theme";
const DARK_THEME_COLOR = "#18181b";
const LIGHT_THEME_COLOR = "#ffffff";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined); const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function isValidTheme(value: string | null): value is Theme { function resolveThemeFromDocument(): Theme {
return value !== null && VALID_THEMES.includes(value as Theme); if (typeof document === "undefined") return "dark";
} return document.documentElement.classList.contains("dark") ? "dark" : "light";
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) { function applyTheme(theme: Theme) {
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
const meta = THEME_META[theme]; const isDark = theme === "dark";
const root = document.documentElement; const root = document.documentElement;
root.classList.toggle("dark", meta.dark); root.classList.toggle("dark", isDark);
root.classList.toggle("theme-tokyo-night", theme === "tokyo-night"); root.style.colorScheme = isDark ? "dark" : "light";
root.style.colorScheme = meta.dark ? "dark" : "light";
const themeColorMeta = document.querySelector('meta[name="theme-color"]'); const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta instanceof HTMLMetaElement) { if (themeColorMeta instanceof HTMLMetaElement) {
themeColorMeta.setAttribute("content", meta.bg); themeColorMeta.setAttribute("content", isDark ? DARK_THEME_COLOR : LIGHT_THEME_COLOR);
} }
} }
export function ThemeProvider({ children }: { children: ReactNode }) { export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => readStoredTheme()); const [theme, setThemeState] = useState<Theme>(() => resolveThemeFromDocument());
const setTheme = useCallback((nextTheme: Theme) => { const setTheme = useCallback((nextTheme: Theme) => {
setThemeState(nextTheme); setThemeState(nextTheme);
}, []); }, []);
const toggleTheme = useCallback(() => { const toggleTheme = useCallback(() => {
setThemeState((current) => { setThemeState((current) => (current === "dark" ? "light" : "dark"));
const idx = VALID_THEMES.indexOf(current);
return VALID_THEMES[(idx + 1) % VALID_THEMES.length];
});
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -74,7 +54,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
try { try {
localStorage.setItem(THEME_STORAGE_KEY, theme); localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch { } catch {
// Ignore localStorage write failures in restricted environments. // Ignore local storage write failures in restricted environments.
} }
}, [theme]); }, [theme]);

View file

@ -45,109 +45,73 @@
:root { :root {
color-scheme: light; color-scheme: light;
--radius: 0; --radius: 0;
--background: #eff1f5; --background: oklch(1 0 0);
--foreground: #4c4f69; --foreground: oklch(0.145 0 0);
--card: #e6e9ef; --card: oklch(1 0 0);
--card-foreground: #4c4f69; --card-foreground: oklch(0.145 0 0);
--popover: #e6e9ef; --popover: oklch(1 0 0);
--popover-foreground: #4c4f69; --popover-foreground: oklch(0.145 0 0);
--primary: #1e66f5; --primary: oklch(0.205 0 0);
--primary-foreground: #eff1f5; --primary-foreground: oklch(0.985 0 0);
--secondary: #ccd0da; --secondary: oklch(0.97 0 0);
--secondary-foreground: #4c4f69; --secondary-foreground: oklch(0.205 0 0);
--muted: #ccd0da; --muted: oklch(0.97 0 0);
--muted-foreground: #9ca0b0; --muted-foreground: oklch(0.556 0 0);
--accent: #bcc0cc; --accent: oklch(0.97 0 0);
--accent-foreground: #4c4f69; --accent-foreground: oklch(0.205 0 0);
--destructive: #d20f39; --destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: #eff1f5; --destructive-foreground: oklch(0.577 0.245 27.325);
--border: #ccd0da; --border: oklch(0.922 0 0);
--input: #ccd0da; --input: oklch(0.922 0 0);
--ring: #1e66f5; --ring: oklch(0.708 0 0);
--chart-1: #1e66f5; --chart-1: oklch(0.646 0.222 41.116);
--chart-2: #40a02b; --chart-2: oklch(0.6 0.118 184.704);
--chart-3: #8839ef; --chart-3: oklch(0.398 0.07 227.392);
--chart-4: #df8e1d; --chart-4: oklch(0.828 0.189 84.429);
--chart-5: #d20f39; --chart-5: oklch(0.769 0.188 70.08);
--sidebar: #e6e9ef; --sidebar: oklch(0.985 0 0);
--sidebar-foreground: #4c4f69; --sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: #1e66f5; --sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: #eff1f5; --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: #ccd0da; --sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: #4c4f69; --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: #ccd0da; --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: #1e66f5; --sidebar-ring: oklch(0.708 0 0);
} }
.dark { .dark {
color-scheme: dark; --background: oklch(0.145 0 0);
--background: #1e1e2e; --foreground: oklch(0.985 0 0);
--foreground: #cdd6f4; --card: oklch(0.205 0 0);
--card: #181825; --card-foreground: oklch(0.985 0 0);
--card-foreground: #cdd6f4; --popover: oklch(0.205 0 0);
--popover: #181825; --popover-foreground: oklch(0.985 0 0);
--popover-foreground: #cdd6f4; --primary: oklch(0.985 0 0);
--primary: #89b4fa; --primary-foreground: oklch(0.205 0 0);
--primary-foreground: #1e1e2e; --secondary: oklch(0.269 0 0);
--secondary: #313244; --secondary-foreground: oklch(0.985 0 0);
--secondary-foreground: #cdd6f4; --muted: oklch(0.269 0 0);
--muted: #313244; --muted-foreground: oklch(0.708 0 0);
--muted-foreground: #6c7086; --accent: oklch(0.269 0 0);
--accent: #45475a; --accent-foreground: oklch(0.985 0 0);
--accent-foreground: #cdd6f4; --destructive: oklch(0.637 0.237 25.331);
--destructive: #f38ba8; --destructive-foreground: oklch(0.985 0 0);
--destructive-foreground: #1e1e2e; --border: oklch(0.269 0 0);
--border: #313244; --input: oklch(0.269 0 0);
--input: #313244; --ring: oklch(0.439 0 0);
--ring: #89b4fa; --chart-1: oklch(0.488 0.243 264.376);
--chart-1: #89b4fa; --chart-2: oklch(0.696 0.17 162.48);
--chart-2: #a6e3a1; --chart-3: oklch(0.769 0.188 70.08);
--chart-3: #cba6f7; --chart-4: oklch(0.627 0.265 303.9);
--chart-4: #f9e2af; --chart-5: oklch(0.645 0.246 16.439);
--chart-5: #f38ba8; --sidebar: oklch(0.145 0 0);
--sidebar: #181825; --sidebar-foreground: oklch(0.985 0 0);
--sidebar-foreground: #cdd6f4; --sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary: #89b4fa; --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-primary-foreground: #1e1e2e; --sidebar-accent: oklch(0.269 0 0);
--sidebar-accent: #313244; --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-accent-foreground: #cdd6f4; --sidebar-border: oklch(0.269 0 0);
--sidebar-border: #313244; --sidebar-ring: oklch(0.439 0 0);
--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 { @layer base {
@ -196,6 +160,10 @@
} }
/* Dark mode scrollbars */ /* Dark mode scrollbars */
.dark {
color-scheme: dark;
}
.dark *::-webkit-scrollbar { .dark *::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { activityApi } from "../api/activity"; import { activityApi } from "../api/activity";
import { agentsApi } from "../api/agents"; import { agentsApi } from "../api/agents";
@ -83,7 +82,7 @@ export function Activity() {
}, [issues]); }, [issues]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={History} message={`Select a ${VOCAB.company.toLowerCase()} to view activity.`} />; return <EmptyState icon={History} message="Select a company to view activity." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -925,7 +925,7 @@ export function AgentDetail() {
{actionError && <p className="text-sm text-destructive">{actionError}</p>} {actionError && <p className="text-sm text-destructive">{actionError}</p>}
{isPendingApproval && ( {isPendingApproval && (
<p className="text-sm text-amber-500"> <p className="text-sm text-amber-500">
This agent is pending owner approval and cannot be invoked yet. This agent is pending board approval and cannot be invoked yet.
</p> </p>
)} )}
@ -1479,11 +1479,11 @@ function ConfigurationTab({
const taskAssignLocked = agent.role === "ceo" || canCreateAgents; const taskAssignLocked = agent.role === "ceo" || canCreateAgents;
const taskAssignHint = const taskAssignHint =
taskAssignSource === "ceo_role" taskAssignSource === "ceo_role"
? `Enabled automatically for ${VOCAB.ceo} agents.` ? "Enabled automatically for CEO agents."
: taskAssignSource === "agent_creator" : taskAssignSource === "agent_creator"
? "Enabled automatically while this agent can create new agents." ? "Enabled automatically while this agent can create new agents."
: taskAssignSource === "explicit_grant" : taskAssignSource === "explicit_grant"
? `Enabled via explicit ${VOCAB.company.toLowerCase()} permission grant.` ? "Enabled via explicit company permission grant."
: "Disabled unless explicitly granted."; : "Disabled unless explicitly granted.";
return ( return (
@ -1727,7 +1727,7 @@ function PromptsTab({
const uploadMarkdownImage = useMutation({ const uploadMarkdownImage = useMutation({
mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => { mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => {
if (!selectedCompanyId) throw new Error(`Select a ${VOCAB.company.toLowerCase()} to upload images`); if (!selectedCompanyId) throw new Error("Select a company to upload images");
return assetsApi.uploadImage(selectedCompanyId, file, namespace); return assetsApi.uploadImage(selectedCompanyId, file, namespace);
}, },
}); });
@ -1927,7 +1927,7 @@ function PromptsTab({
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" /> <HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" sideOffset={4}> <TooltipContent side="right" sideOffset={4}>
{`Managed: ${VOCAB.appName} stores and serves the instructions bundle. External: you provide a path on disk where the instructions live.`} Managed: Paperclip stores and serves the instructions bundle. External: you provide a path on disk where the instructions live.
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</span> </span>
@ -1982,7 +1982,7 @@ function PromptsTab({
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" /> <HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" sideOffset={4}> <TooltipContent side="right" sideOffset={4}>
{`The absolute directory on disk where the instructions bundle lives. In managed mode this is set by ${VOCAB.appName} automatically.`} The absolute directory on disk where the instructions bundle lives. In managed mode this is set by Paperclip automatically.
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</span> </span>
@ -2512,9 +2512,9 @@ function AgentSkillsTab({
const unsupportedSkillMessage = useMemo(() => { const unsupportedSkillMessage = useMemo(() => {
if (skillSnapshot?.mode !== "unsupported") return null; if (skillSnapshot?.mode !== "unsupported") return null;
if (agent.adapterType === "openclaw_gateway") { if (agent.adapterType === "openclaw_gateway") {
return `${VOCAB.appName} cannot manage OpenClaw skills here. Visit your OpenClaw instance to manage this agent's skills.`; return "Paperclip cannot manage OpenClaw skills here. Visit your OpenClaw instance to manage this agent's skills.";
} }
return `${VOCAB.appName} cannot manage skills for this adapter yet. Manage them in the adapter directly.`; return "Paperclip cannot manage skills for this adapter yet. Manage them in the adapter directly.";
}, [agent.adapterType, skillSnapshot?.mode]); }, [agent.adapterType, skillSnapshot?.mode]);
const hasUnsavedChanges = !arraysEqual(skillDraft, lastSavedSkills); const hasUnsavedChanges = !arraysEqual(skillDraft, lastSavedSkills);
const saveStatusLabel = syncSkills.isPending const saveStatusLabel = syncSkills.isPending
@ -2672,7 +2672,7 @@ function AgentSkillsTab({
<section className="border-y border-border"> <section className="border-y border-border">
<div className="border-b border-border bg-muted/40 px-3 py-2"> <div className="border-b border-border bg-muted/40 px-3 py-2">
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
{`Required by ${VOCAB.appName}`} Required by Paperclip
</span> </span>
</div> </div>
{requiredSkillRows.map(renderSkillRow)} {requiredSkillRows.map(renderSkillRow)}
@ -2683,7 +2683,7 @@ function AgentSkillsTab({
<section className="border-y border-border"> <section className="border-y border-border">
<div className="border-b border-border bg-muted/40 px-3 py-2"> <div className="border-b border-border bg-muted/40 px-3 py-2">
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
{`User-installed skills, not managed by ${VOCAB.appName}`} User-installed skills, not managed by Paperclip
</span> </span>
</div> </div>
{unmanagedSkillRows.map(renderSkillRow)} {unmanagedSkillRows.map(renderSkillRow)}
@ -2695,7 +2695,7 @@ function AgentSkillsTab({
{desiredOnlyMissingSkills.length > 0 && ( {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="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 ${VOCAB.company.toLowerCase()} library`}</div> <div className="font-medium">Requested skills missing from the company library</div>
<div className="mt-1 text-xs"> <div className="mt-1 text-xs">
{desiredOnlyMissingSkills.join(", ")} {desiredOnlyMissingSkills.join(", ")}
</div> </div>
@ -3969,7 +3969,7 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
Create API Key Create API Key
</h3> </h3>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{`API keys allow this agent to authenticate calls to the ${VOCAB.appName} server.`} API keys allow this agent to authenticate calls to the Paperclip server.
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useNavigate, useLocation } from "@/lib/router"; import { Link, useNavigate, useLocation } from "@/lib/router";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { agentsApi, type OrgNode } from "../api/agents"; import { agentsApi, type OrgNode } from "../api/agents";
@ -123,7 +122,7 @@ export function Agents() {
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Bot} message={`Select a ${VOCAB.company.toLowerCase()} to view agents.`} />; return <EmptyState icon={Bot} message="Select a company to view agents." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useNavigate, useLocation } from "@/lib/router"; import { useNavigate, useLocation } from "@/lib/router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { approvalsApi } from "../api/approvals"; import { approvalsApi } from "../api/approvals";
@ -76,7 +75,7 @@ export function Approvals() {
).length; ).length;
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <p className="text-sm text-muted-foreground">{`Select a ${VOCAB.company.toLowerCase()} first.`}</p>; return <p className="text-sm text-muted-foreground">Select a company first.</p>;
} }
if (isLoading) { if (isLoading) {

View file

@ -143,7 +143,7 @@ export function CliAuthPage() {
</div> </div>
{challenge.requestedCompanyName && ( {challenge.requestedCompanyName && (
<div> <div>
<div className="text-muted-foreground">{`Requested ${VOCAB.company.toLowerCase()}`}</div> <div className="text-muted-foreground">Requested company</div>
<div className="text-foreground">{challenge.requestedCompanyName}</div> <div className="text-foreground">{challenge.requestedCompanyName}</div>
</div> </div>
)} )}

View file

@ -70,7 +70,7 @@ export function Companies() {
}); });
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: VOCAB.companies }]); setBreadcrumbs([{ label: "Companies" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
function startEdit(companyId: string, currentName: string) { function startEdit(companyId: string, currentName: string) {
@ -98,7 +98,7 @@ export function Companies() {
</div> </div>
<div className="h-6"> <div className="h-6">
{loading && <p className="text-sm text-muted-foreground">{`Loading ${VOCAB.companies.toLowerCase()}...`}</p>} {loading && <p className="text-sm text-muted-foreground">Loading companies...</p>}
{error && <p className="text-sm text-destructive">{error.message}</p>} {error && <p className="text-sm text-destructive">{error.message}</p>}
</div> </div>
@ -267,7 +267,7 @@ export function Companies() {
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<p className="text-sm text-destructive font-medium"> <p className="text-sm text-destructive font-medium">
{`Delete this ${VOCAB.company.toLowerCase()} and all its data? This cannot be undone.`} Delete this company and all its data? This cannot be undone.
</p> </p>
<div className="flex items-center gap-2 ml-4 shrink-0"> <div className="flex items-center gap-2 ml-4 shrink-0">
<Button <Button

View file

@ -912,7 +912,7 @@ export function CompanyExport() {
} }
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Package} message={`Select a ${VOCAB.company.toLowerCase()} to export.`} />; return <EmptyState icon={Package} message="Select a company to export." />;
} }
if (exportPreviewMutation.isPending && !exportData) { if (exportPreviewMutation.isPending && !exportData) {

View file

@ -704,7 +704,7 @@ export function CompanyImport() {
}, [companyAgents]); }, [companyAgents]);
const localZipHelpText = const localZipHelpText =
`Upload a .zip exported directly from ${VOCAB.appName}. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly.`; "Upload a .zip exported directly from Paperclip. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly.";
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
@ -1086,7 +1086,7 @@ export function CompanyImport() {
const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null; const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null;
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Download} message={`Select a ${VOCAB.company.toLowerCase()} to import into.`} />; return <EmptyState icon={Download} message="Select a company to import into." />;
} }
return ( return (
@ -1096,7 +1096,7 @@ export function CompanyImport() {
<div> <div>
<h2 className="text-base font-semibold">Import source</h2> <h2 className="text-base font-semibold">Import source</h2>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{`Choose a GitHub repo or upload a local ${VOCAB.appName} zip package.`} Choose a GitHub repo or upload a local Paperclip zip package.
</p> </p>
</div> </div>
@ -1178,7 +1178,7 @@ export function CompanyImport() {
</Field> </Field>
)} )}
<Field label="Target" hint={`Import into this ${VOCAB.company.toLowerCase()} or create a new one.`}> <Field label="Target" hint="Import into this company or create a new one.">
<select <select
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
value={targetMode} value={targetMode}
@ -1187,16 +1187,16 @@ export function CompanyImport() {
setImportPreview(null); setImportPreview(null);
}} }}
> >
<option value="new">{`Create new ${VOCAB.company.toLowerCase()}`}</option> <option value="new">Create new company</option>
<option value="existing"> <option value="existing">
{`Existing ${VOCAB.company.toLowerCase()}: ${selectedCompany?.name}`} Existing company: {selectedCompany?.name}
</option> </option>
</select> </select>
</Field> </Field>
{targetMode === "new" && ( {targetMode === "new" && (
<Field <Field
label={`New ${VOCAB.company.toLowerCase()} name`} label="New company name"
hint="Optional override. Leave blank to use the package name." hint="Optional override. Leave blank to use the package name."
> >
<input <input
@ -1211,7 +1211,7 @@ export function CompanyImport() {
<Field <Field
label="Collision strategy" label="Collision strategy"
hint={`${VOCAB.board} imports can rename, skip, or replace matching ${VOCAB.company.toLowerCase()} content.`} hint="Board imports can rename, skip, or replace matching company content."
> >
<select <select
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"

View file

@ -208,7 +208,7 @@ export function CompanySettings() {
if (!selectedCompany) { if (!selectedCompany) {
return ( return (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{`No ${VOCAB.company.toLowerCase()} selected. Select a ${VOCAB.company.toLowerCase()} from the switcher above.`} No company selected. Select a company from the switcher above.
</div> </div>
); );
} }

View file

@ -989,7 +989,7 @@ export function CompanySkills() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Boxes} message={`Select a ${VOCAB.company.toLowerCase()} to manage skills.`} />; return <EmptyState icon={Boxes} message="Select a company to manage skills." />;
} }
function handleAddSkillSource() { function handleAddSkillSource() {

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useRef, useState, type ComponentType } from "react"; import { useEffect, useMemo, useRef, useState, type ComponentType } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { import type {
BudgetPolicySummary, BudgetPolicySummary,
@ -36,12 +35,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const NO_COMPANY = "__none__"; const NO_COMPANY = "__none__";
const SCOPE_LABELS: Record<string, string> = {
company: VOCAB.company,
agent: "Agent",
project: "Project",
};
function currentWeekRange(): { from: string; to: string } { function currentWeekRange(): { from: string; to: string } {
const now = new Date(); const now = new Date();
const day = now.getDay(); const day = now.getDay();
@ -536,7 +529,7 @@ export function Costs() {
}), [budgetPolicies]); }), [budgetPolicies]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={DollarSign} message={`Select a ${VOCAB.company.toLowerCase()} to view costs.`} />; return <EmptyState icon={DollarSign} message="Select a company to view costs." />;
} }
const showCustomPrompt = preset === "custom" && !customReady; const showCustomPrompt = preset === "custom" && !customReady;
@ -862,7 +855,7 @@ export function Costs() {
<MetricTile <MetricTile
label="Pending approvals" label="Pending approvals"
value={String(budgetData?.pendingApprovalCount ?? 0)} value={String(budgetData?.pendingApprovalCount ?? 0)}
subtitle="Budget override approvals awaiting owner action" subtitle="Budget override approvals awaiting board action"
icon={ArrowUpRight} icon={ArrowUpRight}
/> />
<MetricTile <MetricTile
@ -914,10 +907,10 @@ export function Costs() {
return ( return (
<section key={scopeType} className="space-y-3"> <section key={scopeType} className="space-y-3">
<div> <div>
<h2 className="text-lg font-semibold">{SCOPE_LABELS[scopeType] ?? scopeType} budgets</h2> <h2 className="text-lg font-semibold capitalize">{scopeType} budgets</h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{scopeType === "company" {scopeType === "company"
? `${VOCAB.company}-wide monthly policy.` ? "Company-wide monthly policy."
: scopeType === "agent" : scopeType === "agent"
? "Recurring monthly spend policies for individual agents." ? "Recurring monthly spend policies for individual agents."
: "Lifetime spend policies for execution-bound projects."} : "Lifetime spend policies for execution-bound projects."}
@ -946,7 +939,7 @@ export function Costs() {
{budgetPolicies.length === 0 ? ( {budgetPolicies.length === 0 ? (
<Card> <Card>
<CardContent className="px-5 py-8 text-sm text-muted-foreground"> <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 ${VOCAB.company.toLowerCase()} monthly budget control.`} No budget policies yet. Set agent and project budgets from their detail pages, or use the existing company monthly budget control.
</CardContent> </CardContent>
</Card> </Card>
) : null} ) : null}

View file

@ -277,8 +277,8 @@ export function Dashboard() {
description={ description={
<span> <span>
{data.budgets.pendingApprovals > 0 {data.budgets.pendingApprovals > 0
? `${data.budgets.pendingApprovals} budget overrides awaiting owner review` ? `${data.budgets.pendingApprovals} budget overrides awaiting board review`
: "Awaiting owner review"} : "Awaiting board review"}
</span> </span>
} }
/> />

View file

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { import {
BookOpen, BookOpen,
Bot, Bot,
@ -195,7 +194,7 @@ export function DesignGuide() {
<div> <div>
<h2 className="text-xl font-bold">Design Guide</h2> <h2 className="text-xl font-bold">Design Guide</h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
{`Every component, style, and pattern used across ${VOCAB.appName}.`} Every component, style, and pattern used across Paperclip.
</p> </p>
</div> </div>
@ -737,7 +736,7 @@ export function DesignGuide() {
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink href="#">{`${VOCAB.appName} App`}</BreadcrumbLink> <BreadcrumbLink href="#">Paperclip App</BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
@ -944,7 +943,7 @@ export function DesignGuide() {
<SubSection title="Initials derivation"> <SubSection title="Initials derivation">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Identity name={`${VOCAB.ceo} Agent`} size="sm" /> <Identity name="CEO Agent" size="sm" />
<Identity name="Alpha" size="sm" /> <Identity name="Alpha" size="sm" />
<Identity name="Quality Assurance Lead" size="sm" /> <Identity name="Quality Assurance Lead" size="sm" />
</div> </div>

View file

@ -1,5 +1,4 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { goalsApi } from "../api/goals"; import { goalsApi } from "../api/goals";
import { useCompany } from "../context/CompanyContext"; import { useCompany } from "../context/CompanyContext";
@ -28,7 +27,7 @@ export function Goals() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Target} message={`Select a ${VOCAB.company.toLowerCase()} to view goals.`} />; return <EmptyState icon={Target} message="Select a company to view goals." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useLocation, useNavigate } from "@/lib/router"; import { Link, useLocation, useNavigate } from "@/lib/router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { approvalsApi } from "../api/approvals"; import { approvalsApi } from "../api/approvals";
@ -910,7 +909,7 @@ export function Inbox() {
}; };
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={InboxIcon} message={`Select a ${VOCAB.company.toLowerCase()} to view inbox.`} />; return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />;
} }
const hasRunFailures = failedRuns.length > 0; const hasRunFailures = failedRuns.length > 0;

View file

@ -3,17 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { SlidersHorizontal } from "lucide-react"; import { SlidersHorizontal } from "lucide-react";
import { instanceSettingsApi } from "@/api/instanceSettings"; import { instanceSettingsApi } from "@/api/instanceSettings";
import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { useTheme, THEME_META, type Theme } from "../context/ThemeContext";
import { queryKeys } from "../lib/queryKeys"; import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
const ORDERED_THEMES: Theme[] = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
export function InstanceGeneralSettings() { export function InstanceGeneralSettings() {
const { setBreadcrumbs } = useBreadcrumbs(); const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [actionError, setActionError] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null);
const { theme, setTheme } = useTheme();
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
@ -73,37 +69,6 @@ export function InstanceGeneralSettings() {
</div> </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"> <section className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Clock3, ExternalLink, Settings } from "lucide-react"; import { Clock3, ExternalLink, Settings } from "lucide-react";
import type { InstanceSchedulerHeartbeatAgent } from "@paperclipai/shared"; import type { InstanceSchedulerHeartbeatAgent } from "@paperclipai/shared";
@ -172,14 +171,14 @@ export function InstanceSettings() {
<h1 className="text-lg font-semibold">Scheduler Heartbeats</h1> <h1 className="text-lg font-semibold">Scheduler Heartbeats</h1>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{`Agents with a timer heartbeat enabled across all of your ${VOCAB.companies.toLowerCase()}.`} Agents with a timer heartbeat enabled across all of your companies.
</p> </p>
</div> </div>
<div className="flex items-center gap-4 text-sm text-muted-foreground"> <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">{activeCount}</span> active</span>
<span><span className="font-semibold text-foreground">{disabledCount}</span> disabled</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 ? VOCAB.company.toLowerCase() : VOCAB.companies.toLowerCase()}</span> <span><span className="font-semibold text-foreground">{grouped.length}</span> {grouped.length === 1 ? "company" : "companies"}</span>
{anyEnabled && ( {anyEnabled && (
<Button <Button
variant="destructive" variant="destructive"

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useCallback } from "react"; import { useEffect, useMemo, useCallback } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useLocation, useSearchParams } from "@/lib/router"; import { useLocation, useSearchParams } from "@/lib/router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { issuesApi } from "../api/issues"; import { issuesApi } from "../api/issues";
@ -94,7 +93,7 @@ export function Issues() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={CircleDot} message={`Select a ${VOCAB.company.toLowerCase()} to view issues.`} />; return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
} }
return ( return (

View file

@ -1,5 +1,4 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { issuesApi } from "../api/issues"; import { issuesApi } from "../api/issues";
import { useCompany } from "../context/CompanyContext"; import { useCompany } from "../context/CompanyContext";
@ -28,7 +27,7 @@ export function MyIssues() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={ListTodo} message={`Select a ${VOCAB.company.toLowerCase()} to view your issues.`} />; return <EmptyState icon={ListTodo} message="Select a company to view your issues." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -331,7 +331,7 @@ export function NewAgent() {
{/* Footer */} {/* Footer */}
<div className="border-t border-border px-4 py-3"> <div className="border-t border-border px-4 py-3">
{isFirstAgent && ( {isFirstAgent && (
<p className="text-xs text-muted-foreground mb-2">{`This will be the ${VOCAB.ceo}`}</p> <p className="text-xs text-muted-foreground mb-2">This will be the CEO</p>
)} )}
{formError && ( {formError && (
<p className="text-xs text-destructive mb-2">{formError}</p> <p className="text-xs text-destructive mb-2">{formError}</p>

View file

@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link } from "@/lib/router"; import { Link } from "@/lib/router";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { agentsApi, type OrgNode } from "../api/agents"; import { agentsApi, type OrgNode } from "../api/agents";
@ -105,7 +104,7 @@ export function Org() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={GitBranch} message={`Select a ${VOCAB.company.toLowerCase()} to view org chart.`} />; return <EmptyState icon={GitBranch} message="Select a company to view org chart." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -1,5 +1,4 @@
import { useEffect, useRef, useState, useMemo, useCallback } from "react"; import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useNavigate } from "@/lib/router"; import { Link, useNavigate } from "@/lib/router";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { agentsApi, type OrgNode } from "../api/agents"; import { agentsApi, type OrgNode } from "../api/agents";
@ -257,7 +256,7 @@ export function OrgChart() {
}, [zoom, pan]); }, [zoom, pan]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Network} message={`Select a ${VOCAB.company.toLowerCase()} to view the org chart.`} />; return <EmptyState icon={Network} message="Select a company to view the org chart." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, Navigate, useParams } from "@/lib/router"; import { Link, Navigate, useParams } from "@/lib/router";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useCompany } from "@/context/CompanyContext"; import { useCompany } from "@/context/CompanyContext";
@ -105,7 +104,7 @@ export function PluginPage() {
} }
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-muted-foreground">{`Select a ${VOCAB.company.toLowerCase()} to view this page.`}</p> <p className="text-sm text-muted-foreground">Select a company to view this page.</p>
</div> </div>
); );
} }

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { projectsApi } from "../api/projects"; import { projectsApi } from "../api/projects";
import { useCompany } from "../context/CompanyContext"; import { useCompany } from "../context/CompanyContext";
@ -34,7 +33,7 @@ export function Projects() {
); );
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Hexagon} message={`Select a ${VOCAB.company.toLowerCase()} to view projects.`} />; return <EmptyState icon={Hexagon} message="Select a company to view projects." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useLocation, useNavigate, useParams } from "@/lib/router"; import { Link, useLocation, useNavigate, useParams } from "@/lib/router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
@ -403,7 +402,7 @@ export function RoutineDetail() {
onError: (error) => { onError: (error) => {
pushToast({ pushToast({
title: "Failed to save routine", title: "Failed to save routine",
body: error instanceof Error ? error.message : `${VOCAB.appName} could not save the routine.`, body: error instanceof Error ? error.message : "Paperclip could not save the routine.",
tone: "error", tone: "error",
}); });
}, },
@ -424,7 +423,7 @@ export function RoutineDetail() {
onError: (error) => { onError: (error) => {
pushToast({ pushToast({
title: "Routine run failed", title: "Routine run failed",
body: error instanceof Error ? error.message : `${VOCAB.appName} could not start the routine run.`, body: error instanceof Error ? error.message : "Paperclip could not start the routine run.",
tone: "error", tone: "error",
}); });
}, },
@ -446,7 +445,7 @@ export function RoutineDetail() {
onError: (error) => { onError: (error) => {
pushToast({ pushToast({
title: "Failed to update routine", title: "Failed to update routine",
body: error instanceof Error ? error.message : `${VOCAB.appName} could not update the routine.`, body: error instanceof Error ? error.message : "Paperclip could not update the routine.",
tone: "error", tone: "error",
}); });
}, },
@ -487,7 +486,7 @@ export function RoutineDetail() {
onError: (error) => { onError: (error) => {
pushToast({ pushToast({
title: "Failed to add trigger", title: "Failed to add trigger",
body: error instanceof Error ? error.message : `${VOCAB.appName} could not create the trigger.`, body: error instanceof Error ? error.message : "Paperclip could not create the trigger.",
tone: "error", tone: "error",
}); });
}, },
@ -505,7 +504,7 @@ export function RoutineDetail() {
onError: (error) => { onError: (error) => {
pushToast({ pushToast({
title: "Failed to update trigger", title: "Failed to update trigger",
body: error instanceof Error ? error.message : `${VOCAB.appName} could not update the trigger.`, body: error instanceof Error ? error.message : "Paperclip could not update the trigger.",
tone: "error", tone: "error",
}); });
}, },
@ -523,7 +522,7 @@ export function RoutineDetail() {
onError: (error) => { onError: (error) => {
pushToast({ pushToast({
title: "Failed to delete trigger", title: "Failed to delete trigger",
body: error instanceof Error ? error.message : `${VOCAB.appName} could not delete the trigger.`, body: error instanceof Error ? error.message : "Paperclip could not delete the trigger.",
tone: "error", tone: "error",
}); });
}, },
@ -545,7 +544,7 @@ export function RoutineDetail() {
onError: (error) => { onError: (error) => {
pushToast({ pushToast({
title: "Failed to rotate webhook secret", title: "Failed to rotate webhook secret",
body: error instanceof Error ? error.message : `${VOCAB.appName} could not rotate the webhook secret.`, body: error instanceof Error ? error.message : "Paperclip could not rotate the webhook secret.",
tone: "error", tone: "error",
}); });
}, },
@ -585,7 +584,7 @@ export function RoutineDetail() {
const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null; const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null;
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Repeat} message={`Select a ${VOCAB.company.toLowerCase()} to view routines.`} />; return <EmptyState icon={Repeat} message="Select a company to view routines." />;
} }
if (isLoading) { if (isLoading) {
@ -674,7 +673,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 className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-4 space-y-3 text-sm">
<div> <div>
<p className="font-medium">{secretMessage.title}</p> <p className="font-medium">{secretMessage.title}</p>
<p className="text-xs text-muted-foreground">{`Save this now. ${VOCAB.appName} will not show the secret value again.`}</p> <p className="text-xs text-muted-foreground">Save this now. Paperclip will not show the secret value again.</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View file

@ -219,7 +219,7 @@ export function Routines() {
const currentProject = draft.projectId ? projectById.get(draft.projectId) ?? null : null; const currentProject = draft.projectId ? projectById.get(draft.projectId) ?? null : null;
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Repeat} message={`Select a ${VOCAB.company.toLowerCase()} to view routines.`} />; return <EmptyState icon={Repeat} message="Select a company to view routines." />;
} }
if (isLoading) { if (isLoading) {

View file

@ -6,20 +6,13 @@ import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
resolve: { resolve: {
alias: [ alias: {
{ find: "@", replacement: path.resolve(__dirname, "./src") }, "@": path.resolve(__dirname, "./src"),
{ lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
find: "lexical", // [nexus] Replace upstream OnboardingWizard with Nexus single-step version
replacement: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"), [path.resolve(__dirname, "src/components/OnboardingWizard")]:
}, path.resolve(__dirname, "./src/components/NexusOnboardingWizard"),
// [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: { server: {
port: 5173, port: 5173,