Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 45 additions & 8 deletions packages/app/api/lib/route-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export async function routeInboundMessage(input: InboundMessage): Promise<RouteR
allowList?: string[];
clawscale?: { name?: string; answerStyle?: string; isActive?: boolean; rateLimit?: { maxMessages: number; windowSeconds: number }; llm?: AgentLlmConfig };
blockList?: string[];
backendLabels?: 'show' | 'hide' | 'force-hide';
};

// 3. Find or create EndUser
Expand Down Expand Up @@ -159,6 +160,14 @@ export async function routeInboundMessage(input: InboundMessage): Promise<RouteR
const clawscaleLlm = clawscaleCfg.llm ?? { model: 'openai:gpt-5.4-mini' };
const clawscaleRateLimit = clawscaleCfg.rateLimit;

// Resolve backend label visibility
const labelPolicy = settings.backendLabels ?? 'show';
const userMeta = (endUser.metadata ?? {}) as Record<string, unknown>;
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' },
Expand Down Expand Up @@ -219,16 +228,19 @@ export async function routeInboundMessage(input: InboundMessage): Promise<RouteR

// ── Helper closures ─────────────────────────────────────────────────

function formatCombined(entries: ReplyEntry[]): string {
return entries.map((r) =>
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<RouteResult> {
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) {
Expand Down Expand Up @@ -295,10 +307,7 @@ export async function routeInboundMessage(input: InboundMessage): Promise<RouteR
}
}
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) };
}

// 8. Parse commands
Expand Down Expand Up @@ -505,6 +514,28 @@ export async function routeInboundMessage(input: InboundMessage): Promise<RouteR
return reply(`*Linked accounts:*\n\n${lines.join('\n')}`);
}

case 'labels': {
const labelPolicy = settings.backendLabels ?? 'show';
if (labelPolicy === 'force-hide') {
return reply('Backend labels are disabled by the admin and cannot be changed.');
}
// Toggle the user's preference stored in endUser.metadata
const userMeta = (endUser!.metadata ?? {}) as Record<string, unknown>;
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(
Expand Down Expand Up @@ -597,6 +628,12 @@ export async function routeInboundMessage(input: InboundMessage): Promise<RouteR
return result;
}

// Assistant disabled, no active backends — show available backends or a nudge
if (allBackends.length > 0) {
const list = allBackends.map((b, i) => `${i + 1}. ${b.name}`).join('\n');
return reply(`Use \`/team invite <name|#>\` to add an agent:\n\n${list}`);
}

return null;
}

Expand Down
5 changes: 3 additions & 2 deletions packages/app/api/lib/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -40,7 +40,7 @@ export type ParsedCommand = SystemCommand | DirectMessage;

// ── Parser ────────────────────────────────────────────────────────────────────

const SYSTEM_COMMANDS = new Set<CommandType>(['backends', 'clear', 'team', 'help', 'link', 'unlink', 'linked', 'deleteaccount']);
const SYSTEM_COMMANDS = new Set<CommandType>(['backends', 'clear', 'team', 'help', 'link', 'unlink', 'linked', 'deleteaccount', 'labels']);

/**
* Parse user text for commands.
Expand Down Expand Up @@ -99,6 +99,7 @@ export const COMMAND_REFERENCE = [
{ command: '/link <code>', 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;
Expand Down
1 change: 1 addition & 0 deletions packages/app/api/routes/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
7 changes: 7 additions & 0 deletions packages/app/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
43 changes: 43 additions & 0 deletions packages/app/web/app/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function Settings() {
const [siteTitle, setSiteTitle] = useState('');
const [logoUrl, setLogoUrl] = useState('');
const [endUserAccess, setEndUserAccess] = useState<TenantSettings['endUserAccess']>('anonymous');
const [clawscaleEnabled, setClawscaleEnabled] = useState(true);
const [clawscaleModel, setClawscaleModel] = useState('openai:gpt-5.4-mini');
const [clawscaleApiKey, setClawscaleApiKey] = useState('');
const [apiKeySet, setApiKeySet] = useState(false);
Expand All @@ -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<ApiResponse<Tenant>>('/api/tenant').then((res) => {
Expand All @@ -41,13 +43,15 @@ 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);
const rl = s.clawscale?.rateLimit;
if (rl) { setRateLimitEnabled(true); setRateLimitMax(rl.maxMessages); setRateLimitWindow(rl.windowSeconds); }
setDefaultHomePage(s.defaultHomePage ?? '/dashboard');
setAllowRegistration(s.allowRegistration !== false);
setBackendLabels(s.backendLabels ?? 'show');
}
setLoading(false);
});
Expand All @@ -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 } : {}),
Expand Down Expand Up @@ -143,6 +149,14 @@ export default function Settings() {
<h2 className="font-semibold text-gray-900 mb-1">ClawScale Setup Assistant</h2>
<p className="text-sm text-gray-500 mb-4">Configure the built-in AI assistant that helps end-users navigate your bot.</p>
<div className="space-y-4">
<label className="flex items-start gap-3 cursor-pointer">
<input type="checkbox" checked={clawscaleEnabled} onChange={(e) => setClawscaleEnabled(e.target.checked)} disabled={!isAdmin} className="mt-0.5" />
<span>
<span className="text-sm font-medium text-gray-900">Enable ClawScale assistant</span>
<span className="text-xs text-gray-500 block">When disabled, the AI assistant will not respond to any messages or commands. System commands like /team and /backends still work.</span>
</span>
</label>
{clawscaleEnabled && (<>
<div>
<label className="label">Model</label>
<input className="input" placeholder="openai:gpt-5.4-mini" value={clawscaleModel} onChange={(e) => setClawscaleModel(e.target.value)} disabled={!isAdmin} />
Expand Down Expand Up @@ -179,6 +193,7 @@ export default function Settings() {
</div>
</div>
)}
</>)}
</div>
</div>

Expand Down Expand Up @@ -210,6 +225,34 @@ export default function Settings() {
</div>
</div>

<div className="card p-6">
<h2 className="font-semibold text-gray-900 mb-1">Backend Labels</h2>
<p className="text-sm text-gray-500 mb-4">Control whether the AI backend name (e.g. [GPT-4]) is shown in responses to end-users.</p>
<div className="space-y-2">
{([
['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]) => (
<label key={value} className="flex items-start gap-3 cursor-pointer">
<input
type="radio"
name="backendLabels"
value={value}
checked={backendLabels === value}
onChange={() => setBackendLabels(value)}
disabled={!isAdmin}
className="mt-0.5"
/>
<span>
<span className="text-sm font-medium text-gray-900">{label}</span>
<span className="text-xs text-gray-500 block">{desc}</span>
</span>
</label>
))}
</div>
</div>

{isAdmin && (
<div className="flex items-center gap-4">
<button type="submit" className="btn-primary" disabled={saving}>
Expand Down
Loading