Merge remote-tracking branch 'public-gh/master'

* public-gh/master:
  fix: disable secure cookies for HTTP deployments
  feat(adapters): add claude-sonnet-4-6 and claude-haiku-4-6 models
  Add opencode-ai to global npm install in Dockerfile
  fix: correct env var priority for authDisableSignUp
  Add pi-local package.json to Dockerfile
  feat: add auth.disableSignUp config option
  refactor: extract roleLabels to shared constants
  fix(secrets): add secretKeys tracking to resolveEnvBindings for consistent redaction
  fix(db): reuse MIGRATIONS_FOLDER constant instead of recomputing
  fix(server): wake agent when issue status changes from backlog
  fix(server): use home-based path for run logs instead of cwd
  fix(db): use fileURLToPath for Windows-safe migration paths
  fix(server): auto-deduplicate agent names on creation instead of rejecting
  feat(ui): show human-readable role labels in agent list and properties
  fix(ui): prevent blank screen when prompt template is emptied
  fix(server): redact secret-sourced env vars in run logs by provenance
  fix(cli): split path and query in buildUrl to prevent %3F encoding
  fix(scripts): use shell on Windows to fix spawn EINVAL in dev-runner
This commit is contained in:
Dotta
2026-03-09 07:29:34 -05:00
21 changed files with 98 additions and 42 deletions

View File

@@ -18,6 +18,8 @@ COPY packages/adapters/codex-local/package.json packages/adapters/codex-local/
COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/ COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/
COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/ COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/
COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/ COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
FROM base AS build FROM base AS build
@@ -31,7 +33,7 @@ RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" &
FROM base AS production FROM base AS production
WORKDIR /app WORKDIR /app
COPY --from=build /app /app COPY --from=build /app /app
RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai
ENV NODE_ENV=production \ ENV NODE_ENV=production \
HOME=/paperclip \ HOME=/paperclip \

View File

@@ -104,8 +104,10 @@ export class PaperclipApiClient {
function buildUrl(apiBase: string, path: string): string { function buildUrl(apiBase: string, path: string): string {
const normalizedPath = path.startsWith("/") ? path : `/${path}`; const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const [pathname, query] = normalizedPath.split("?");
const url = new URL(apiBase); const url = new URL(apiBase);
url.pathname = `${url.pathname.replace(/\/+$/, "")}${normalizedPath}`; url.pathname = `${url.pathname.replace(/\/+$/, "")}${pathname}`;
if (query) url.search = query;
return url.toString(); return url.toString();
} }

View File

@@ -3,6 +3,8 @@ export const label = "Claude Code (local)";
export const models = [ export const models = [
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" }, { id: "claude-opus-4-6", label: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
{ id: "claude-haiku-4-6", label: "Claude Haiku 4.6" },
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" }, { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" }, { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
]; ];

View File

@@ -2,12 +2,13 @@ import { createHash } from "node:crypto";
import { drizzle as drizzlePg } from "drizzle-orm/postgres-js"; import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator"; import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
import { readFile, readdir } from "node:fs/promises"; import { readFile, readdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import postgres from "postgres"; import postgres from "postgres";
import * as schema from "./schema/index.js"; import * as schema from "./schema/index.js";
const MIGRATIONS_FOLDER = new URL("./migrations", import.meta.url).pathname; const MIGRATIONS_FOLDER = fileURLToPath(new URL("./migrations", import.meta.url));
const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations"; const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations";
const MIGRATIONS_JOURNAL_JSON = new URL("./migrations/meta/_journal.json", import.meta.url).pathname; const MIGRATIONS_JOURNAL_JSON = fileURLToPath(new URL("./migrations/meta/_journal.json", import.meta.url));
function isSafeIdentifier(value: string): boolean { function isSafeIdentifier(value: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value); return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
@@ -702,8 +703,7 @@ export async function migratePostgresIfEmpty(url: string): Promise<MigrationBoot
} }
const db = drizzlePg(sql); const db = drizzlePg(sql);
const migrationsFolder = new URL("./migrations", import.meta.url).pathname; await migratePg(db, { migrationsFolder: MIGRATIONS_FOLDER });
await migratePg(db, { migrationsFolder });
return { migrated: true, reason: "migrated-empty-db", tableCount: 0 }; return { migrated: true, reason: "migrated-empty-db", tableCount: 0 };
} finally { } finally {

View File

@@ -55,6 +55,7 @@ export const serverConfigSchema = z.object({
export const authConfigSchema = z.object({ export const authConfigSchema = z.object({
baseUrlMode: z.enum(AUTH_BASE_URL_MODES).default("auto"), baseUrlMode: z.enum(AUTH_BASE_URL_MODES).default("auto"),
publicBaseUrl: z.string().url().optional(), publicBaseUrl: z.string().url().optional(),
disableSignUp: z.boolean().default(false),
}); });
export const storageLocalDiskConfigSchema = z.object({ export const storageLocalDiskConfigSchema = z.object({
@@ -103,6 +104,7 @@ export const paperclipConfigSchema = z
server: serverConfigSchema, server: serverConfigSchema,
auth: authConfigSchema.default({ auth: authConfigSchema.default({
baseUrlMode: "auto", baseUrlMode: "auto",
disableSignUp: false,
}), }),
storage: storageConfigSchema.default({ storage: storageConfigSchema.default({
provider: "local_disk", provider: "local_disk",

View File

@@ -48,6 +48,20 @@ export const AGENT_ROLES = [
] as const; ] as const;
export type AgentRole = (typeof AGENT_ROLES)[number]; export type AgentRole = (typeof AGENT_ROLES)[number];
export const AGENT_ROLE_LABELS: Record<AgentRole, string> = {
ceo: "CEO",
cto: "CTO",
cmo: "CMO",
cfo: "CFO",
engineer: "Engineer",
designer: "Designer",
pm: "PM",
qa: "QA",
devops: "DevOps",
researcher: "Researcher",
general: "General",
};
export const AGENT_ICON_NAMES = [ export const AGENT_ICON_NAMES = [
"bot", "bot",
"cpu", "cpu",

View File

@@ -6,6 +6,7 @@ export {
AGENT_STATUSES, AGENT_STATUSES,
AGENT_ADAPTER_TYPES, AGENT_ADAPTER_TYPES,
AGENT_ROLES, AGENT_ROLES,
AGENT_ROLE_LABELS,
AGENT_ICON_NAMES, AGENT_ICON_NAMES,
ISSUE_STATUSES, ISSUE_STATUSES,
ISSUE_PRIORITIES, ISSUE_PRIORITIES,

View File

@@ -47,7 +47,7 @@ const serverScript = mode === "watch" ? "dev:watch" : "dev";
const child = spawn( const child = spawn(
pnpmBin, pnpmBin,
["--filter", "@paperclipai/server", serverScript, ...forwardedArgs], ["--filter", "@paperclipai/server", serverScript, ...forwardedArgs],
{ stdio: "inherit", env }, { stdio: "inherit", env, shell: process.platform === "win32" },
); );
child.on("exit", (code, signal) => { child.on("exit", (code, signal) => {

View File

@@ -70,6 +70,9 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins?
const secret = process.env.BETTER_AUTH_SECRET ?? process.env.PAPERCLIP_AGENT_JWT_SECRET ?? "paperclip-dev-secret"; const secret = process.env.BETTER_AUTH_SECRET ?? process.env.PAPERCLIP_AGENT_JWT_SECRET ?? "paperclip-dev-secret";
const effectiveTrustedOrigins = trustedOrigins ?? deriveAuthTrustedOrigins(config); const effectiveTrustedOrigins = trustedOrigins ?? deriveAuthTrustedOrigins(config);
const publicUrl = process.env.PAPERCLIP_PUBLIC_URL ?? baseUrl;
const isHttpOnly = publicUrl ? publicUrl.startsWith("http://") : false;
const authConfig = { const authConfig = {
baseURL: baseUrl, baseURL: baseUrl,
secret, secret,
@@ -86,7 +89,9 @@ export function createBetterAuthInstance(db: Db, config: Config, trustedOrigins?
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
requireEmailVerification: false, requireEmailVerification: false,
disableSignUp: config.authDisableSignUp,
}, },
...(isHttpOnly ? { advanced: { useSecureCookies: false } } : {}),
}; };
if (!baseUrl) { if (!baseUrl) {

View File

@@ -37,6 +37,7 @@ export interface Config {
allowedHostnames: string[]; allowedHostnames: string[];
authBaseUrlMode: AuthBaseUrlMode; authBaseUrlMode: AuthBaseUrlMode;
authPublicBaseUrl: string | undefined; authPublicBaseUrl: string | undefined;
authDisableSignUp: boolean;
databaseMode: DatabaseMode; databaseMode: DatabaseMode;
databaseUrl: string | undefined; databaseUrl: string | undefined;
embeddedPostgresDataDir: string; embeddedPostgresDataDir: string;
@@ -142,6 +143,11 @@ export function loadConfig(): Config {
authBaseUrlModeFromEnv ?? authBaseUrlModeFromEnv ??
fileConfig?.auth?.baseUrlMode ?? fileConfig?.auth?.baseUrlMode ??
(authPublicBaseUrl ? "explicit" : "auto"); (authPublicBaseUrl ? "explicit" : "auto");
const disableSignUpFromEnv = process.env.PAPERCLIP_AUTH_DISABLE_SIGN_UP;
const authDisableSignUp: boolean =
disableSignUpFromEnv !== undefined
? disableSignUpFromEnv === "true"
: (fileConfig?.auth?.disableSignUp ?? false);
const allowedHostnamesFromEnvRaw = process.env.PAPERCLIP_ALLOWED_HOSTNAMES; const allowedHostnamesFromEnvRaw = process.env.PAPERCLIP_ALLOWED_HOSTNAMES;
const allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw const allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw
? allowedHostnamesFromEnvRaw ? allowedHostnamesFromEnvRaw
@@ -203,6 +209,7 @@ export function loadConfig(): Config {
allowedHostnames, allowedHostnames,
authBaseUrlMode, authBaseUrlMode,
authPublicBaseUrl, authPublicBaseUrl,
authDisableSignUp,
databaseMode: fileDatabaseMode, databaseMode: fileDatabaseMode,
databaseUrl: process.env.DATABASE_URL ?? fileDbUrl, databaseUrl: process.env.DATABASE_URL ?? fileDbUrl,
embeddedPostgresDataDir: resolveHomeAwarePath( embeddedPostgresDataDir: resolveHomeAwarePath(

View File

@@ -245,7 +245,7 @@ export function agentRoutes(db: Db) {
adapterConfig: Record<string, unknown>, adapterConfig: Record<string, unknown>,
) { ) {
if (adapterType !== "opencode_local") return; if (adapterType !== "opencode_local") return;
const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig); const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig);
const runtimeEnv = asRecord(runtimeConfig.env) ?? {}; const runtimeEnv = asRecord(runtimeConfig.env) ?? {};
try { try {
await ensureOpenCodeModelConfiguredAndAvailable({ await ensureOpenCodeModelConfiguredAndAvailable({
@@ -420,7 +420,7 @@ export function agentRoutes(db: Db) {
inputAdapterConfig, inputAdapterConfig,
{ strictMode: strictSecretsMode }, { strictMode: strictSecretsMode },
); );
const runtimeAdapterConfig = await secretsSvc.resolveAdapterConfigForRuntime( const { config: runtimeAdapterConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
companyId, companyId,
normalizedAdapterConfig, normalizedAdapterConfig,
); );
@@ -1264,7 +1264,7 @@ export function agentRoutes(db: Db) {
} }
const config = asRecord(agent.adapterConfig) ?? {}; const config = asRecord(agent.adapterConfig) ?? {};
const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config); const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config);
const result = await runClaudeLogin({ const result = await runClaudeLogin({
runId: `claude-login-${randomUUID()}`, runId: `claude-login-${randomUUID()}`,
agent: { agent: {

View File

@@ -575,6 +575,10 @@ export function issueRoutes(db: Db, storage: StorageService) {
} }
const assigneeChanged = assigneeWillChange; const assigneeChanged = assigneeWillChange;
const statusChangedFromBacklog =
existing.status === "backlog" &&
issue.status !== "backlog" &&
req.body.status !== undefined;
// Merge all wakeups from this update into one enqueue per agent to avoid duplicate runs. // Merge all wakeups from this update into one enqueue per agent to avoid duplicate runs.
void (async () => { void (async () => {
@@ -592,6 +596,18 @@ export function issueRoutes(db: Db, storage: StorageService) {
}); });
} }
if (!assigneeChanged && statusChangedFromBacklog && issue.assigneeAgentId) {
wakeups.set(issue.assigneeAgentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_status_changed",
payload: { issueId: issue.id, mutation: "update" },
requestedByActorType: actor.actorType,
requestedByActorId: actor.actorId,
contextSnapshot: { issueId: issue.id, source: "issue.status_change" },
});
}
if (commentBody && comment) { if (commentBody && comment) {
let mentionedIds: string[] = []; let mentionedIds: string[] = [];
try { try {

View File

@@ -341,13 +341,17 @@ export function agentService(db: Db) {
await ensureManager(companyId, data.reportsTo); await ensureManager(companyId, data.reportsTo);
} }
await assertCompanyShortnameAvailable(companyId, data.name); const existingAgents = await db
.select({ id: agents.id, name: agents.name, status: agents.status })
.from(agents)
.where(eq(agents.companyId, companyId));
const uniqueName = deduplicateAgentName(data.name, existingAgents);
const role = data.role ?? "general"; const role = data.role ?? "general";
const normalizedPermissions = normalizeAgentPermissions(data.permissions, role); const normalizedPermissions = normalizeAgentPermissions(data.permissions, role);
const created = await db const created = await db
.insert(agents) .insert(agents)
.values({ ...data, companyId, role, permissions: normalizedPermissions }) .values({ ...data, name: uniqueName, companyId, role, permissions: normalizedPermissions })
.returning() .returning()
.then((rows) => rows[0]); .then((rows) => rows[0]);

View File

@@ -1240,11 +1240,16 @@ export function heartbeatService(db: Db) {
const mergedConfig = issueAssigneeOverrides?.adapterConfig const mergedConfig = issueAssigneeOverrides?.adapterConfig
? { ...config, ...issueAssigneeOverrides.adapterConfig } ? { ...config, ...issueAssigneeOverrides.adapterConfig }
: config; : config;
const resolvedConfig = await secretsSvc.resolveAdapterConfigForRuntime( const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(
agent.companyId, agent.companyId,
mergedConfig, mergedConfig,
); );
const onAdapterMeta = async (meta: AdapterInvocationMeta) => { const onAdapterMeta = async (meta: AdapterInvocationMeta) => {
if (meta.env && secretKeys.size > 0) {
for (const key of secretKeys) {
if (key in meta.env) meta.env[key] = "***REDACTED***";
}
}
await appendRunEvent(currentRun, seq++, { await appendRunEvent(currentRun, seq++, {
eventType: "adapter.invoke", eventType: "adapter.invoke",
stream: "system", stream: "system",

View File

@@ -2,6 +2,7 @@ import { createReadStream, promises as fs } from "node:fs";
import path from "node:path"; import path from "node:path";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { notFound } from "../errors.js"; import { notFound } from "../errors.js";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
export type RunLogStoreType = "local_file"; export type RunLogStoreType = "local_file";
@@ -148,7 +149,7 @@ let cachedStore: RunLogStore | null = null;
export function getRunLogStore() { export function getRunLogStore() {
if (cachedStore) return cachedStore; if (cachedStore) return cachedStore;
const basePath = process.env.RUN_LOG_BASE_PATH ?? path.resolve(process.cwd(), "data/run-logs"); const basePath = process.env.RUN_LOG_BASE_PATH ?? path.resolve(resolvePaperclipInstanceRoot(), "data", "run-logs");
cachedStore = createLocalFileRunLogStore(basePath); cachedStore = createLocalFileRunLogStore(basePath);
return cachedStore; return cachedStore;
} }

View File

@@ -308,10 +308,11 @@ export function secretService(db: Db) {
return normalized; return normalized;
}, },
resolveEnvBindings: async (companyId: string, envValue: unknown) => { resolveEnvBindings: async (companyId: string, envValue: unknown): Promise<{ env: Record<string, string>; secretKeys: Set<string> }> => {
const record = asRecord(envValue); const record = asRecord(envValue);
if (!record) return {} as Record<string, string>; if (!record) return { env: {} as Record<string, string>, secretKeys: new Set<string>() };
const resolved: Record<string, string> = {}; const resolved: Record<string, string> = {};
const secretKeys = new Set<string>();
for (const [key, rawBinding] of Object.entries(record)) { for (const [key, rawBinding] of Object.entries(record)) {
if (!ENV_KEY_RE.test(key)) { if (!ENV_KEY_RE.test(key)) {
@@ -326,20 +327,22 @@ export function secretService(db: Db) {
resolved[key] = binding.value; resolved[key] = binding.value;
} else { } else {
resolved[key] = await resolveSecretValue(companyId, binding.secretId, binding.version); resolved[key] = await resolveSecretValue(companyId, binding.secretId, binding.version);
secretKeys.add(key);
} }
} }
return resolved; return { env: resolved, secretKeys };
}, },
resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record<string, unknown>) => { resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record<string, unknown>): Promise<{ config: Record<string, unknown>; secretKeys: Set<string> }> => {
const resolved = { ...adapterConfig }; const resolved = { ...adapterConfig };
const secretKeys = new Set<string>();
if (!Object.prototype.hasOwnProperty.call(adapterConfig, "env")) { if (!Object.prototype.hasOwnProperty.call(adapterConfig, "env")) {
return resolved; return { config: resolved, secretKeys };
} }
const record = asRecord(adapterConfig.env); const record = asRecord(adapterConfig.env);
if (!record) { if (!record) {
resolved.env = {}; resolved.env = {};
return resolved; return { config: resolved, secretKeys };
} }
const env: Record<string, string> = {}; const env: Record<string, string> = {};
for (const [key, rawBinding] of Object.entries(record)) { for (const [key, rawBinding] of Object.entries(record)) {
@@ -355,10 +358,11 @@ export function secretService(db: Db) {
env[key] = binding.value; env[key] = binding.value;
} else { } else {
env[key] = await resolveSecretValue(companyId, binding.secretId, binding.version); env[key] = await resolveSecretValue(companyId, binding.secretId, binding.version);
secretKeys.add(key);
} }
} }
resolved.env = env; resolved.env = env;
return resolved; return { config: resolved, secretKeys };
}, },
}; };
} }

View File

@@ -441,7 +441,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
"promptTemplate", "promptTemplate",
String(config.promptTemplate ?? ""), String(config.promptTemplate ?? ""),
)} )}
onChange={(v) => mark("adapterConfig", "promptTemplate", v || undefined)} onChange={(v) => mark("adapterConfig", "promptTemplate", v ?? "")}
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
contentClassName="min-h-[88px] text-sm font-mono" contentClassName="min-h-[88px] text-sm font-mono"
imageUploadHandler={async (file) => { imageUploadHandler={async (file) => {

View File

@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Link } from "@/lib/router"; import { Link } from "@/lib/router";
import type { Agent, AgentRuntimeState } from "@paperclipai/shared"; import { AGENT_ROLE_LABELS, type Agent, type AgentRuntimeState } from "@paperclipai/shared";
import { agentsApi } from "../api/agents"; import { agentsApi } from "../api/agents";
import { useCompany } from "../context/CompanyContext"; import { useCompany } from "../context/CompanyContext";
import { queryKeys } from "../lib/queryKeys"; import { queryKeys } from "../lib/queryKeys";
@@ -24,6 +24,8 @@ const adapterLabels: Record<string, string> = {
http: "HTTP", http: "HTTP",
}; };
const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) { function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<div className="flex items-center gap-3 py-1.5"> <div className="flex items-center gap-3 py-1.5">
@@ -51,7 +53,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) {
<StatusBadge status={agent.status} /> <StatusBadge status={agent.status} />
</PropertyRow> </PropertyRow>
<PropertyRow label="Role"> <PropertyRow label="Role">
<span className="text-sm">{agent.role}</span> <span className="text-sm">{roleLabels[agent.role] ?? agent.role}</span>
</PropertyRow> </PropertyRow>
{agent.title && ( {agent.title && (
<PropertyRow label="Title"> <PropertyRow label="Title">

View File

@@ -15,6 +15,7 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { HelpCircle, ChevronDown, ChevronRight } from "lucide-react"; import { HelpCircle, ChevronDown, ChevronRight } from "lucide-react";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { AGENT_ROLE_LABELS } from "@paperclipai/shared";
/* ---- Help text for (?) tooltips ---- */ /* ---- Help text for (?) tooltips ---- */
export const help: Record<string, string> = { export const help: Record<string, string> = {
@@ -59,11 +60,7 @@ export const adapterLabels: Record<string, string> = {
http: "HTTP", http: "HTTP",
}; };
export const roleLabels: Record<string, string> = { export const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO",
engineer: "Engineer", designer: "Designer", pm: "PM",
qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General",
};
/* ---- Primitive components ---- */ /* ---- Primitive components ---- */

View File

@@ -18,7 +18,7 @@ import { PageTabBar } from "../components/PageTabBar";
import { Tabs } from "@/components/ui/tabs"; import { Tabs } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Bot, Plus, List, GitBranch, SlidersHorizontal } from "lucide-react"; import { Bot, Plus, List, GitBranch, SlidersHorizontal } from "lucide-react";
import type { Agent } from "@paperclipai/shared"; import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared";
const adapterLabels: Record<string, string> = { const adapterLabels: Record<string, string> = {
claude_local: "Claude", claude_local: "Claude",
@@ -30,11 +30,7 @@ const adapterLabels: Record<string, string> = {
http: "HTTP", http: "HTTP",
}; };
const roleLabels: Record<string, string> = { const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO",
engineer: "Engineer", designer: "Designer", pm: "PM",
qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General",
};
type FilterTab = "all" | "active" | "paused" | "error"; type FilterTab = "all" | "active" | "paused" | "error";
@@ -230,7 +226,7 @@ export function Agents() {
<EntityRow <EntityRow
key={agent.id} key={agent.id}
title={agent.name} title={agent.name}
subtitle={`${agent.role}${agent.title ? ` - ${agent.title}` : ""}`} subtitle={`${roleLabels[agent.role] ?? agent.role}${agent.title ? ` - ${agent.title}` : ""}`}
to={agentUrl(agent)} to={agentUrl(agent)}
leading={ leading={
<span className="relative flex h-2.5 w-2.5"> <span className="relative flex h-2.5 w-2.5">

View File

@@ -10,7 +10,7 @@ import { EmptyState } from "../components/EmptyState";
import { PageSkeleton } from "../components/PageSkeleton"; import { PageSkeleton } from "../components/PageSkeleton";
import { AgentIcon } from "../components/AgentIconPicker"; import { AgentIcon } from "../components/AgentIconPicker";
import { Network } from "lucide-react"; import { Network } from "lucide-react";
import type { Agent } from "@paperclipai/shared"; import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared";
// Layout constants // Layout constants
const CARD_W = 200; const CARD_W = 200;
@@ -421,11 +421,7 @@ export function OrgChart() {
); );
} }
const roleLabels: Record<string, string> = { const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO",
engineer: "Engineer", designer: "Designer", pm: "PM",
qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General",
};
function roleLabel(role: string): string { function roleLabel(role: string): string {
return roleLabels[role] ?? role; return roleLabels[role] ?? role;