diff --git a/package.json b/package.json index f6645bd..391b5d5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "test": "pnpm -r test", "db:push": "pnpm --filter @clawscale/app db:push", "db:migrate": "pnpm --filter @clawscale/app db:migrate", - "db:studio": "pnpm --filter @clawscale/app db:studio" + "db:studio": "pnpm --filter @clawscale/app db:studio", + "start": "pnpm --parallel -r --filter \"!@clawscale/cli-bridge\" start" }, "devDependencies": { "concurrently": "^9.2.1", diff --git a/packages/app/api/lib/route-message.ts b/packages/app/api/lib/route-message.ts index ce50af6..6ca475b 100644 --- a/packages/app/api/lib/route-message.ts +++ b/packages/app/api/lib/route-message.ts @@ -97,6 +97,7 @@ export async function routeInboundMessage(input: InboundMessage): Promise; + const userHideLabels = userMeta.hideLabels as boolean | undefined; + const hideLabels = labelPolicy === 'force-hide' + || (labelPolicy === 'hide' && userHideLabels !== false) + || (labelPolicy === 'show' && userHideLabels === true); + const allBackends = await db.aiBackend.findMany({ where: { tenantId, isActive: true }, orderBy: { createdAt: 'asc' }, @@ -219,16 +228,19 @@ export async function routeInboundMessage(input: InboundMessage): Promise + r.backendName && !hideLabels ? `[${r.backendName}]\n${r.reply}` : r.reply, + ).join('\n\n---\n\n'); + } + async function reply(content: string, backendId: string | null = null, backendName: string | null = clawscaleName): Promise { replies.push({ backendId, backendName, reply: content }); await db.message.create({ data: { id: generateId('msg'), conversationId: conversation!.id, role: 'assistant', content, backendId }, }); await db.conversation.update({ where: { id: conversation!.id }, data: { updatedAt: new Date() } }); - const combined = replies.map((r) => - r.backendName ? `[${r.backendName}]\n${r.reply}` : r.reply, - ).join('\n\n---\n\n'); - return { conversationId: conversation!.id, replies, reply: combined }; + return { conversationId: conversation!.id, replies, reply: formatCombined(replies) }; } async function addBackend(backendId: string) { @@ -295,10 +307,7 @@ export async function routeInboundMessage(input: InboundMessage): Promise - r.backendName ? `[${r.backendName}]\n${r.reply}` : r.reply, - ).join('\n\n---\n\n'); - return { conversationId: conversation!.id, replies, reply: combined }; + return { conversationId: conversation!.id, replies, reply: formatCombined(replies) }; } // 8. Parse commands @@ -505,6 +514,28 @@ export async function routeInboundMessage(input: InboundMessage): Promise; + const currentPref = userMeta.hideLabels as boolean | undefined; + // Default depends on policy: 'show' → labels visible, 'hide' → labels hidden + const defaultHidden = labelPolicy === 'hide'; + const currentlyHidden = currentPref ?? defaultHidden; + const newHidden = !currentlyHidden; + await db.endUser.update({ + where: { id: endUser!.id }, + data: { metadata: { ...userMeta, hideLabels: newHidden } }, + }); + return reply(newHidden + ? '✅ Backend labels are now *hidden* in responses. Use `/labels` to show them again.' + : '✅ Backend labels are now *visible* in responses. Use `/labels` to hide them again.', + ); + } + case 'deleteaccount': { if (cmd.arg.toLowerCase() !== 'confirm') { return reply( @@ -597,6 +628,12 @@ export async function routeInboundMessage(input: InboundMessage): Promise 0) { + const list = allBackends.map((b, i) => `${i + 1}. ${b.name}`).join('\n'); + return reply(`Use \`/team invite \` to add an agent:\n\n${list}`); + } + return null; } diff --git a/packages/app/api/lib/slash-commands.ts b/packages/app/api/lib/slash-commands.ts index 276932b..d7dabc6 100644 --- a/packages/app/api/lib/slash-commands.ts +++ b/packages/app/api/lib/slash-commands.ts @@ -19,7 +19,7 @@ // ── Types ───────────────────────────────────────────────────────────────────── -export type CommandType = 'backends' | 'clear' | 'team' | 'help' | 'link' | 'unlink' | 'linked' | 'deleteaccount'; +export type CommandType = 'backends' | 'clear' | 'team' | 'help' | 'link' | 'unlink' | 'linked' | 'deleteaccount' | 'labels'; export interface SystemCommand { kind: 'system'; @@ -40,7 +40,7 @@ export type ParsedCommand = SystemCommand | DirectMessage; // ── Parser ──────────────────────────────────────────────────────────────────── -const SYSTEM_COMMANDS = new Set(['backends', 'clear', 'team', 'help', 'link', 'unlink', 'linked', 'deleteaccount']); +const SYSTEM_COMMANDS = new Set(['backends', 'clear', 'team', 'help', 'link', 'unlink', 'linked', 'deleteaccount', 'labels']); /** * Parse user text for commands. @@ -99,6 +99,7 @@ export const COMMAND_REFERENCE = [ { command: '/link ', description: 'link this channel to another using a code' }, { command: '/unlink', description: 'remove the link from this channel' }, { command: '/linked', description: 'show all linked accounts and channels' }, + { command: '/labels', description: 'toggle backend name labels on or off in responses' }, { command: '/deleteaccount', description: 'permanently delete your account and all data' }, { command: '/help', description: 'show all commands' }, ] as const; diff --git a/packages/app/api/routes/tenant.ts b/packages/app/api/routes/tenant.ts index 01baca7..527549a 100644 --- a/packages/app/api/routes/tenant.ts +++ b/packages/app/api/routes/tenant.ts @@ -30,6 +30,7 @@ const updateSettingsSchema = z.object({ }).optional(), defaultHomePage: z.string().max(100).nullable().optional(), allowRegistration: z.boolean().optional(), + backendLabels: z.enum(['show', 'hide', 'force-hide']).optional(), onboarding: z.object({ headline: z.string().max(200).optional(), subtitle: z.string().max(400).optional(), diff --git a/packages/app/shared/index.ts b/packages/app/shared/index.ts index 8932f42..131a371 100644 --- a/packages/app/shared/index.ts +++ b/packages/app/shared/index.ts @@ -77,6 +77,13 @@ export interface TenantSettings { defaultHomePage?: string; /** Whether new users can register and join this project (default: true) */ allowRegistration?: boolean; + /** + * Backend name label policy for end-user responses. + * - show: always display [BackendName] prefix (default) + * - hide: hide by default, user can toggle on + * - force-hide: always hidden, user cannot override + */ + backendLabels?: 'show' | 'hide' | 'force-hide'; } export type AiBackendType = 'llm' | 'openclaw' | 'palmos' | 'claude-code' | 'custom' | 'cli-bridge'; diff --git a/packages/app/web/app/pages/Settings.tsx b/packages/app/web/app/pages/Settings.tsx index 830a9ef..cb27ff4 100644 --- a/packages/app/web/app/pages/Settings.tsx +++ b/packages/app/web/app/pages/Settings.tsx @@ -22,6 +22,7 @@ export default function Settings() { const [siteTitle, setSiteTitle] = useState(''); const [logoUrl, setLogoUrl] = useState(''); const [endUserAccess, setEndUserAccess] = useState('anonymous'); + const [clawscaleEnabled, setClawscaleEnabled] = useState(true); const [clawscaleModel, setClawscaleModel] = useState('openai:gpt-5.4-mini'); const [clawscaleApiKey, setClawscaleApiKey] = useState(''); const [apiKeySet, setApiKeySet] = useState(false); @@ -31,6 +32,7 @@ export default function Settings() { const [rateLimitWindow, setRateLimitWindow] = useState(60); const [defaultHomePage, setDefaultHomePage] = useState('/dashboard'); const [allowRegistration, setAllowRegistration] = useState(true); + const [backendLabels, setBackendLabels] = useState<'show' | 'hide' | 'force-hide'>('show'); useEffect(() => { api.get>('/api/tenant').then((res) => { @@ -41,6 +43,7 @@ export default function Settings() { setSiteTitle(s.siteTitle ?? ''); setLogoUrl(s.logoUrl ?? ''); setEndUserAccess(s.endUserAccess ?? 'anonymous'); + setClawscaleEnabled(s.clawscale?.isActive !== false); setClawscaleModel(s.clawscale?.llm?.model ?? 'openai:gpt-5.4-mini'); setApiKeySet(!!s.clawscale?.llm?.apiKey && s.clawscale.llm.apiKey !== ''); setClawscaleMultimodal(s.clawscale?.llm?.multimodal ?? false); @@ -48,6 +51,7 @@ export default function Settings() { if (rl) { setRateLimitEnabled(true); setRateLimitMax(rl.maxMessages); setRateLimitWindow(rl.windowSeconds); } setDefaultHomePage(s.defaultHomePage ?? '/dashboard'); setAllowRegistration(s.allowRegistration !== false); + setBackendLabels(s.backendLabels ?? 'show'); } setLoading(false); }); @@ -63,8 +67,10 @@ export default function Settings() { logoUrl: logoUrl || null, defaultHomePage: defaultHomePage || null, allowRegistration, + backendLabels, endUserAccess, clawscale: { + isActive: clawscaleEnabled, llm: { model: clawscaleModel, ...(clawscaleApiKey ? { apiKey: clawscaleApiKey } : {}), @@ -143,6 +149,14 @@ export default function Settings() {

ClawScale Setup Assistant

Configure the built-in AI assistant that helps end-users navigate your bot.

+ + {clawscaleEnabled && (<>
setClawscaleModel(e.target.value)} disabled={!isAdmin} /> @@ -179,6 +193,7 @@ export default function Settings() {
)} + )} @@ -210,6 +225,34 @@ export default function Settings() { +
+

Backend Labels

+

Control whether the AI backend name (e.g. [GPT-4]) is shown in responses to end-users.

+
+ {([ + ['show', 'Always show', 'Display [BackendName] prefix on every response.'], + ['hide', 'Hide by default', 'Labels are hidden but users can toggle them on.'], + ['force-hide', 'Always hide', 'Labels are always hidden. Users cannot override this.'], + ] as const).map(([value, label, desc]) => ( + + ))} +
+
+ {isAdmin && (