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,
resolveAgentJwtEnvFile,
} from "../config/env.js";
import { VOCAB } from "@paperclipai/branding";
import type { CheckResult } from "./index.js";
export function agentJwtSecretCheck(configPath?: string): CheckResult {
@ -24,7 +23,7 @@ export function agentJwtSecretCheck(configPath?: string): CheckResult {
name: "Agent JWT secret",
status: "warn",
message: `PAPERCLIP_AGENT_JWT_SECRET is present in ${envPath} but not loaded into environment`,
repairHint: `Set the value from ${envPath} in your shell before starting the ${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 { VOCAB } from "@paperclipai/branding";
import type { CheckResult } from "./index.js";
function isLoopbackHost(host: string) {
@ -38,7 +37,7 @@ export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
status: "fail",
message: "authenticated mode requires BETTER_AUTH_SECRET (or PAPERCLIP_AGENT_JWT_SECRET)",
canRepair: false,
repairHint: `Set BETTER_AUTH_SECRET before starting ${VOCAB.appName}`,
repairHint: "Set BETTER_AUTH_SECRET before starting Paperclip",
};
}

View file

@ -1,5 +1,4 @@
import * as p from "@clack/prompts";
import { VOCAB } from "@paperclipai/branding";
import pc from "picocolors";
import { normalizeHostnameInput } from "../config/hostnames.js";
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
@ -28,7 +27,7 @@ export async function addAllowedHostname(host: string, opts: { config?: string }
} else {
p.log.success(`Added allowed hostname: ${pc.cyan(normalized)}`);
p.log.message(
pc.dim(`Restart the ${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 { VOCAB } from "@paperclipai/branding";
import type { ActivityEvent } from "@paperclipai/shared";
import {
addCommonClientOptions,
@ -23,8 +22,8 @@ export function registerActivityCommands(program: Command): void {
addCommonClientOptions(
activity
.command("list")
.description(`List ${VOCAB.company.toLowerCase()} activity log entries`)
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
.description("List company activity log entries")
.requiredOption("-C, --company-id <id>", "Company ID")
.option("--agent-id <id>", "Filter by agent ID")
.option("--entity-type <type>", "Filter by entity type")
.option("--entity-id <id>", "Filter by entity ID")

View file

@ -1,5 +1,4 @@
import { Command } from "commander";
import { VOCAB } from "@paperclipai/branding";
import type { Agent } from "@paperclipai/shared";
import {
removeMaintainerOnlySkillSymlinks,
@ -163,8 +162,8 @@ export function registerAgentCommands(program: Command): void {
addCommonClientOptions(
agent
.command("list")
.description(`List agents for a ${VOCAB.company.toLowerCase()}`)
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
.description("List agents for a company")
.requiredOption("-C, --company-id <id>", "Company ID")
.action(async (opts: AgentListOptions) => {
try {
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",
)
.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(
"--no-install-skills",

View file

@ -1,5 +1,4 @@
import { Command } from "commander";
import { VOCAB } from "@paperclipai/branding";
import {
createApprovalSchema,
requestApprovalRevisionSchema,
@ -49,8 +48,8 @@ export function registerApprovalCommands(program: Command): void {
addCommonClientOptions(
approval
.command("list")
.description(`List approvals for a ${VOCAB.company.toLowerCase()}`)
.requiredOption("-C, --company-id <id>", `${VOCAB.company} ID`)
.description("List approvals for a company")
.requiredOption("-C, --company-id <id>", "Company ID")
.option("--status <status>", "Status filter")
.action(async (opts: ApprovalListOptions) => {
try {
@ -111,7 +110,7 @@ export function registerApprovalCommands(program: Command): void {
approval
.command("create")
.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("--payload <json>", "Approval payload as JSON object")
.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: "issues", label: "Tasks", hint: "tasks and recurring routines" },
{ 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;
@ -1046,7 +1046,7 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions(
company
.command("list")
.description(`List ${VOCAB.companies.toLowerCase()}`) // [nexus]
.description("List companies")
.action(async (opts: CompanyCommandOptions) => {
try {
const ctx = resolveCommandContext(opts);
@ -1081,8 +1081,8 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions(
company
.command("get")
.description(`Get one ${VOCAB.company.toLowerCase()}`) // [nexus]
.argument("<companyId>", `${VOCAB.company} ID`) // [nexus]
.description("Get one company")
.argument("<companyId>", "Company ID")
.action(async (companyId: string, opts: CompanyCommandOptions) => {
try {
const ctx = resolveCommandContext(opts);
@ -1097,8 +1097,8 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions(
company
.command("export")
.description(`Export a ${VOCAB.company.toLowerCase()} into a portable markdown package`) // [nexus]
.argument("<companyId>", `${VOCAB.company} ID`) // [nexus]
.description("Export a company into a portable markdown package")
.argument("<companyId>", "Company ID")
.requiredOption("--out <path>", "Output directory")
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "company,agents")
.option("--skills <values>", "Comma-separated skill slugs/keys to export")
@ -1373,8 +1373,8 @@ export function registerCompanyCommands(program: Command): void {
addCommonClientOptions(
company
.command("delete")
.description(`Delete a ${VOCAB.company.toLowerCase()} by ID or shortname/prefix (destructive)`) // [nexus]
.argument("<selector>", `${VOCAB.company} ID or issue prefix (for example PAP)`) // [nexus]
.description("Delete a company by ID or shortname/prefix (destructive)")
.argument("<selector>", "Company ID or issue prefix (for example PAP)")
.option(
"--by <mode>",
"Selector mode: auto | id | prefix",
@ -1383,7 +1383,7 @@ export function registerCompanyCommands(program: Command): void {
.option("--yes", "Required safety flag to confirm destructive action", false)
.option(
"--confirm <value>",
`Required safety value: target ${VOCAB.company.toLowerCase()} ID or shortname/prefix`, // [nexus]
"Required safety value: target company ID or shortname/prefix",
)
.action(async (selector: string, opts: CompanyDeleteOptions) => {
try {
@ -1425,7 +1425,7 @@ export function registerCompanyCommands(program: Command): void {
} catch (error) {
if (error instanceof ApiRequestError && error.status === 403 && error.message.includes("Board access required")) {
throw new Error(
`${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;

View file

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

View file

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

View file

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

View file

@ -77,7 +77,6 @@ import {
type PlannedIssueDocumentMerge,
type PlannedIssueInsert,
} from "./worktree-merge-history-lib.js";
import { VOCAB } from "@paperclipai/branding";
type WorktreeInitOptions = {
name?: string;
@ -1539,7 +1538,7 @@ async function resolveMergeCompany(input: {
}
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
@ -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)")
.option("--from <worktree>", "Source worktree path, directory name, branch name, or current")
.option("--to <worktree>", "Target worktree path, directory name, branch name, or current (defaults to current)")
.option("--company <id-or-prefix>", `Shared ${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("--apply", "Apply the import after previewing the plan", 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";
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 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_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> {
const now = new Date();
@ -757,7 +757,7 @@ function isMainModule(metaUrl: string): boolean {
if (isMainModule(import.meta.url)) {
void startServer().catch((err) => {
logger.error({ err }, "Nexus server failed to start"); // [nexus]
logger.error({ err }, "Paperclip server failed to start");
process.exit(1);
});
}

View file

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

View file

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

View file

@ -1,65 +1,142 @@
import { test, expect } from "@playwright/test";
/**
* E2E: Nexus onboarding wizard single-step root directory flow.
* E2E: Onboarding wizard flow (skip_llm mode).
*
* Verifies:
* ONBD-10 Vite alias intercepts, NexusOnboardingWizard renders
* ONBD-11 Single root-directory input only, no multi-step flow
* ONBD-12 No corporate placeholder text visible
* Walks through the 4-step OnboardingWizard:
* Step 1 Name your company
* Step 2 Create your first agent (adapter selection + config)
* Step 3 Give it something to do (task creation)
* Step 4 Ready to launch (summary + open issue)
*
* By default this runs in skip_llm mode: we do NOT assert that an LLM
* heartbeat fires. Set PAPERCLIP_E2E_SKIP_LLM=false to enable LLM-dependent
* assertions (requires a valid ANTHROPIC_API_KEY).
*/
test.describe("Nexus onboarding wizard", () => {
test("single-step flow: root dir input, no corporate strings, lands on dashboard", async ({ page }) => {
const SKIP_LLM = process.env.PAPERCLIP_E2E_SKIP_LLM !== "false";
const COMPANY_NAME = `E2E-Test-${Date.now()}`;
const AGENT_NAME = "CEO";
const TASK_TITLE = "E2E test task";
test.describe("Onboarding wizard", () => {
test("completes full wizard flow", async ({ page }) => {
await page.goto("/");
// ONBD-10 + ONBD-11: Nexus wizard renders with single-step heading
const heading = page.locator("h1", { hasText: "Welcome to Nexus" });
await expect(heading).toBeVisible({ timeout: 15_000 });
const wizardHeading = page.locator("h3", { hasText: "Name your company" });
const newCompanyBtn = page.getByRole("button", { name: "New Company" });
// ONBD-11: Only a root directory input — no multi-step navigation
await expect(page.getByRole("button", { name: "Next" })).toHaveCount(0);
await expect(page.locator("h3", { hasText: "Name your company" })).toHaveCount(0);
await expect(page.locator("h3", { hasText: "Create your first agent" })).toHaveCount(0);
await expect(page.locator("h3", { hasText: "Give it something to do" })).toHaveCount(0);
await expect(page.locator("h3", { hasText: "Ready to launch" })).toHaveCount(0);
await expect(
wizardHeading.or(newCompanyBtn)
).toBeVisible({ timeout: 15_000 });
// ONBD-12: No corporate placeholder text
await expect(page.getByText("Acme Corp")).toHaveCount(0);
await expect(page.getByText("Company name")).toHaveCount(0);
await expect(page.getByText("What is this company trying to achieve?")).toHaveCount(0);
if (await newCompanyBtn.isVisible()) {
await newCompanyBtn.click();
}
// Fill root directory and submit
const rootDirInput = page.locator('input[placeholder="~/projects/my-project"]');
await expect(rootDirInput).toBeVisible();
await rootDirInput.fill("/tmp/nexus-e2e-test");
await expect(wizardHeading).toBeVisible({ timeout: 5_000 });
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
await expect(page).toHaveURL(/\/dashboard/, { timeout: 30_000 });
const nextButton = page.getByRole("button", { name: "Next" });
await nextButton.click();
await expect(
page.locator("h3", { hasText: "Create your first agent" })
).toBeVisible({ timeout: 10_000 });
const agentNameInput = page.locator('input[placeholder="CEO"]');
await expect(agentNameInput).toHaveValue(AGENT_NAME);
await expect(
page.locator("button", { hasText: "Claude Code" }).locator("..")
).toBeVisible();
await page.getByRole("button", { name: "More Agent Adapter Types" }).click();
await expect(page.getByRole("button", { name: "Process" })).toHaveCount(0);
await page.getByRole("button", { name: "Next" }).click();
await expect(
page.locator("h3", { hasText: "Give it something to do" })
).toBeVisible({ timeout: 10_000 });
const taskTitleInput = page.locator(
'input[placeholder="e.g. Research competitor pricing"]'
);
await taskTitleInput.clear();
await taskTitleInput.fill(TASK_TITLE);
await page.getByRole("button", { name: "Next" }).click();
await expect(
page.locator("h3", { hasText: "Ready to launch" })
).toBeVisible({ timeout: 10_000 });
await expect(page.locator("text=" + COMPANY_NAME)).toBeVisible();
await expect(page.locator("text=" + AGENT_NAME)).toBeVisible();
await expect(page.locator("text=" + TASK_TITLE)).toBeVisible();
await page.getByRole("button", { name: "Create & Open Issue" }).click();
await expect(page).toHaveURL(/\/issues\//, { timeout: 10_000 });
// Verify workspace and agents created via API
const baseUrl = page.url().split("/").slice(0, 3).join("/");
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
expect(companiesRes.ok()).toBe(true);
const companies = await companiesRes.json();
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(
`${baseUrl}/api/companies/${companyId}/agents`
`${baseUrl}/api/companies/${company.id}/agents`
);
expect(agentsRes.ok()).toBe(true);
const agents = await agentsRes.json();
const ceoAgent = agents.find(
(a: { name: string }) => a.name === AGENT_NAME
);
expect(ceoAgent).toBeTruthy();
expect(ceoAgent.role).toBe("ceo");
expect(ceoAgent.adapterType).not.toBe("process");
// 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(
agents.some((a: { name: string }) => a.name === "Project Manager")
).toBe(true);
expect(
agents.some((a: { name: string }) => a.name === "Engineer")
).toBe(true);
instructionsBundle.files.map((file: { path: string }) => file.path).sort()
).toEqual(["AGENTS.md", "HEARTBEAT.md", "SOUL.md", "TOOLS.md"]);
const issuesRes = await page.request.get(
`${baseUrl}/api/companies/${company.id}/issues`
);
expect(issuesRes.ok()).toBe(true);
const issues = await issuesRes.json();
const task = issues.find(
(i: { title: string }) => i.title === TASK_TITLE
);
expect(task).toBeTruthy();
expect(task.assigneeAgentId).toBe(ceoAgent.id);
expect(task.description).toContain(
"You are the CEO. You set the direction for the company."
);
expect(task.description).not.toContain("github.com/paperclipai/companies");
if (!SKIP_LLM) {
await expect(async () => {
const res = await page.request.get(
`${baseUrl}/api/issues/${task.id}`
);
const issue = await res.json();
expect(["in_progress", "done"]).toContain(issue.status);
}).toPass({ timeout: 120_000, intervals: [5_000] });
}
});
});

View file

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

View file

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

View file

@ -1,5 +1,4 @@
import { useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Eye, EyeOff } from "lucide-react";
import type { AdapterConfigFieldsProps } from "../types";
import {
@ -135,7 +134,7 @@ export function OpenClawGatewayConfigFields({
{!isCreate && (
<>
<Field label={`${VOCAB.appName} API URL override`}>
<Field label="Paperclip API URL override">
<DraftInput
value={
eff(
@ -227,7 +226,7 @@ export function OpenClawGatewayConfigFields({
<Field label="Device auth">
<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.
</div>
</Field>

View file

@ -1,5 +1,4 @@
import { Database, Gauge, ReceiptText } from "lucide-react";
import { VOCAB } from "@paperclipai/branding";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
const SURFACES = [
@ -35,7 +34,7 @@ export function AccountingModelCard() {
Accounting model
</CardTitle>
<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.
</CardDescription>
</CardHeader>

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared";
import type {
@ -187,7 +186,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const createSecret = useMutation({
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);
},
onSuccess: () => {
@ -198,7 +197,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const uploadMarkdownImage = useMutation({
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);
},
});
@ -360,7 +359,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const testEnvironment = useMutation({
mutationFn: async () => {
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, {
adapterConfig: buildAdapterConfigForTest(),

View file

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

View file

@ -1,5 +1,4 @@
import React, { useCallback, useMemo, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import {
ChevronDown,
ChevronRight,
@ -495,7 +494,7 @@ const SecretField = React.memo(({
label={label}
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}
error={error}

View file

@ -20,7 +20,7 @@ import { useDialog } from "../context/DialogContext";
import { usePanel } from "../context/PanelContext";
import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext";
import { useTheme, THEME_META } from "../context/ThemeContext";
import { useTheme } from "../context/ThemeContext";
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
import { healthApi } from "../api/health";
@ -59,12 +59,6 @@ export function Layout() {
setSelectedCompanyId,
} = useCompany();
const { theme, toggleTheme } = useTheme();
const THEME_CYCLE: Record<string, string> = {
"catppuccin-mocha": "Tokyo Night",
"tokyo-night": "Catppuccin Latte",
"catppuccin-latte": "Catppuccin Mocha",
};
const nextThemeLabel = THEME_CYCLE[theme] ?? "next theme";
const { companyPrefix } = useParams<{ companyPrefix: string }>();
const navigate = useNavigate();
const location = useLocation();
@ -73,7 +67,7 @@ export function Layout() {
const lastMainScrollTop = useRef(0);
const [mobileNavVisible, setMobileNavVisible] = useState(true);
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
const isDarkTheme = THEME_META[theme].dark;
const nextTheme = theme === "dark" ? "light" : "dark";
const matchedCompany = useMemo(() => {
if (!companyPrefix) return null;
const requestedPrefix = companyPrefix.toUpperCase();
@ -337,10 +331,10 @@ export function Layout() {
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextThemeLabel}`}
title={`Switch to ${nextThemeLabel}`}
aria-label={`Switch to ${nextTheme} mode`}
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>
</div>
</div>
@ -395,10 +389,10 @@ export function Layout() {
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextThemeLabel}`}
title={`Switch to ${nextThemeLabel}`}
aria-label={`Switch to ${nextTheme} mode`}
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>
</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 remarkGfm from "remark-gfm";
import { cn } from "../lib/utils";
import { useTheme, THEME_META } from "../context/ThemeContext";
import { useTheme } from "../context/ThemeContext";
import { mentionChipInlineStyle, parseMentionChipHref } from "../lib/mention-chips";
interface MarkdownBodyProps {
@ -97,7 +97,7 @@ export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownB
pre: ({ node: _node, children: preChildren, ...preProps }) => {
const mermaidSource = extractMermaidSource(preChildren);
if (mermaidSource) {
return <MermaidDiagramBlock source={mermaidSource} darkMode={THEME_META[theme].dark} />;
return <MermaidDiagramBlock source={mermaidSource} darkMode={theme === "dark"} />;
}
return <pre {...preProps}>{preChildren}</pre>;
},
@ -140,7 +140,7 @@ export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownB
<div
className={cn(
"paperclip-markdown prose prose-sm max-w-none break-words overflow-hidden",
THEME_META[theme].dark && "prose-invert",
theme === "dark" && "prose-invert",
className,
)}
>

View file

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

View file

@ -1,5 +1,4 @@
import { useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link } from "@/lib/router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { Project } from "@paperclipai/shared";
@ -688,7 +687,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
{codebase.effectiveLocalFolder}
</div>
{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 className="flex items-center gap-1">
@ -720,7 +719,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
{hasAdditionalLegacyWorkspaces && (
<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>
)}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query";
import { activityApi } from "../api/activity";
import { agentsApi } from "../api/agents";
@ -83,7 +82,7 @@ export function Activity() {
}, [issues]);
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) {

View file

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

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useNavigate, useLocation } from "@/lib/router";
import { useQuery } from "@tanstack/react-query";
import { agentsApi, type OrgNode } from "../api/agents";
@ -123,7 +122,7 @@ export function Agents() {
}, [setBreadcrumbs]);
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) {

View file

@ -1,5 +1,4 @@
import { useEffect, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useNavigate, useLocation } from "@/lib/router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { approvalsApi } from "../api/approvals";
@ -76,7 +75,7 @@ export function Approvals() {
).length;
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) {

View file

@ -143,7 +143,7 @@ export function CliAuthPage() {
</div>
{challenge.requestedCompanyName && (
<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>
)}

View file

@ -70,7 +70,7 @@ export function Companies() {
});
useEffect(() => {
setBreadcrumbs([{ label: VOCAB.companies }]);
setBreadcrumbs([{ label: "Companies" }]);
}, [setBreadcrumbs]);
function startEdit(companyId: string, currentName: string) {
@ -98,7 +98,7 @@ export function Companies() {
</div>
<div className="h-6">
{loading && <p className="text-sm text-muted-foreground">{`Loading ${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>}
</div>
@ -267,7 +267,7 @@ export function Companies() {
onClick={(e) => e.stopPropagation()}
>
<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>
<div className="flex items-center gap-2 ml-4 shrink-0">
<Button

View file

@ -912,7 +912,7 @@ export function CompanyExport() {
}
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) {

View file

@ -704,7 +704,7 @@ export function CompanyImport() {
}, [companyAgents]);
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(() => {
setBreadcrumbs([
@ -1086,7 +1086,7 @@ export function CompanyImport() {
const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null;
if (!selectedCompanyId) {
return <EmptyState icon={Download} message={`Select a ${VOCAB.company.toLowerCase()} to import into.`} />;
return <EmptyState icon={Download} message="Select a company to import into." />;
}
return (
@ -1096,7 +1096,7 @@ export function CompanyImport() {
<div>
<h2 className="text-base font-semibold">Import source</h2>
<p className="text-xs text-muted-foreground mt-1">
{`Choose a GitHub repo or upload a local ${VOCAB.appName} zip package.`}
Choose a GitHub repo or upload a local Paperclip zip package.
</p>
</div>
@ -1178,7 +1178,7 @@ export function CompanyImport() {
</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
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
value={targetMode}
@ -1187,16 +1187,16 @@ export function CompanyImport() {
setImportPreview(null);
}}
>
<option value="new">{`Create new ${VOCAB.company.toLowerCase()}`}</option>
<option value="new">Create new company</option>
<option value="existing">
{`Existing ${VOCAB.company.toLowerCase()}: ${selectedCompany?.name}`}
Existing company: {selectedCompany?.name}
</option>
</select>
</Field>
{targetMode === "new" && (
<Field
label={`New ${VOCAB.company.toLowerCase()} name`}
label="New company name"
hint="Optional override. Leave blank to use the package name."
>
<input
@ -1211,7 +1211,7 @@ export function CompanyImport() {
<Field
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
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) {
return (
<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>
);
}

View file

@ -989,7 +989,7 @@ export function CompanySkills() {
});
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() {

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
import { useEffect } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query";
import { goalsApi } from "../api/goals";
import { useCompany } from "../context/CompanyContext";
@ -28,7 +27,7 @@ export function Goals() {
});
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) {

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useLocation, useNavigate } from "@/lib/router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { approvalsApi } from "../api/approvals";
@ -910,7 +909,7 @@ export function Inbox() {
};
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;

View file

@ -3,17 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { SlidersHorizontal } from "lucide-react";
import { instanceSettingsApi } from "@/api/instanceSettings";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { useTheme, THEME_META, type Theme } from "../context/ThemeContext";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
const ORDERED_THEMES: Theme[] = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
export function InstanceGeneralSettings() {
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const [actionError, setActionError] = useState<string | null>(null);
const { theme, setTheme } = useTheme();
useEffect(() => {
setBreadcrumbs([
@ -73,37 +69,6 @@ export function InstanceGeneralSettings() {
</div>
)}
<section className="rounded-xl border border-border bg-card p-5">
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">Theme</h2>
<p className="text-sm text-muted-foreground">Choose the visual theme for Nexus.</p>
</div>
<div className="mt-4 flex flex-wrap gap-3">
{ORDERED_THEMES.map((id) => {
const meta = THEME_META[id];
return (
<button
key={id}
type="button"
onClick={() => setTheme(id)}
className={cn(
"flex items-center gap-2.5 rounded-lg border px-3 py-2 text-sm transition-colors",
theme === id
? "border-primary bg-primary/10 text-primary"
: "border-border bg-card text-foreground hover:border-ring",
)}
>
<span
className="h-4 w-4 rounded-full border border-border/50 shrink-0"
style={{ backgroundColor: meta.primary }}
/>
{meta.label}
</button>
);
})}
</div>
</section>
<section className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">

View file

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

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useCallback } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useLocation, useSearchParams } from "@/lib/router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { issuesApi } from "../api/issues";
@ -94,7 +93,7 @@ export function Issues() {
});
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 (

View file

@ -1,5 +1,4 @@
import { useEffect } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query";
import { issuesApi } from "../api/issues";
import { useCompany } from "../context/CompanyContext";
@ -28,7 +27,7 @@ export function MyIssues() {
});
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) {

View file

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

View file

@ -1,5 +1,4 @@
import { useEffect, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link } from "@/lib/router";
import { useQuery } from "@tanstack/react-query";
import { agentsApi, type OrgNode } from "../api/agents";
@ -105,7 +104,7 @@ export function Org() {
});
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) {

View file

@ -1,5 +1,4 @@
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useNavigate } from "@/lib/router";
import { useQuery } from "@tanstack/react-query";
import { agentsApi, type OrgNode } from "../api/agents";
@ -257,7 +256,7 @@ export function OrgChart() {
}, [zoom, pan]);
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) {

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, Navigate, useParams } from "@/lib/router";
import { useQuery } from "@tanstack/react-query";
import { useCompany } from "@/context/CompanyContext";
@ -105,7 +104,7 @@ export function PluginPage() {
}
return (
<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>
);
}

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query";
import { projectsApi } from "../api/projects";
import { useCompany } from "../context/CompanyContext";
@ -34,7 +33,7 @@ export function Projects() {
);
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) {

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { VOCAB } from "@paperclipai/branding";
import { Link, useLocation, useNavigate, useParams } from "@/lib/router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
@ -403,7 +402,7 @@ export function RoutineDetail() {
onError: (error) => {
pushToast({
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",
});
},
@ -424,7 +423,7 @@ export function RoutineDetail() {
onError: (error) => {
pushToast({
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",
});
},
@ -446,7 +445,7 @@ export function RoutineDetail() {
onError: (error) => {
pushToast({
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",
});
},
@ -487,7 +486,7 @@ export function RoutineDetail() {
onError: (error) => {
pushToast({
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",
});
},
@ -505,7 +504,7 @@ export function RoutineDetail() {
onError: (error) => {
pushToast({
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",
});
},
@ -523,7 +522,7 @@ export function RoutineDetail() {
onError: (error) => {
pushToast({
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",
});
},
@ -545,7 +544,7 @@ export function RoutineDetail() {
onError: (error) => {
pushToast({
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",
});
},
@ -585,7 +584,7 @@ export function RoutineDetail() {
const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null;
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) {
@ -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>
<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 className="space-y-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;
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) {

View file

@ -6,20 +6,13 @@ import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: [
{ find: "@", replacement: path.resolve(__dirname, "./src") },
{
find: "lexical",
replacement: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
},
// [nexus] Replace upstream OnboardingWizard with Nexus single-step version.
// RegExp required: string keys match the RAW import specifier (not resolved path).
// App.tsx imports './components/OnboardingWizard' — must match exactly.
{
find: /^\.\/components\/OnboardingWizard$/,
replacement: path.resolve(__dirname, "./src/components/NexusOnboardingWizard"),
},
],
alias: {
"@": path.resolve(__dirname, "./src"),
lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
// [nexus] Replace upstream OnboardingWizard with Nexus single-step version
[path.resolve(__dirname, "src/components/OnboardingWizard")]:
path.resolve(__dirname, "./src/components/NexusOnboardingWizard"),
},
},
server: {
port: 5173,