Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ ss-nodes.json
.agents/
_agent/
_agents/
.trae/

# Internal docs
docs/
3 changes: 2 additions & 1 deletion frontend/src/components/ChannelConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,8 @@ export default function ChannelConfig({ mode, agentId, canManage = true, values,
<span style={{ color: 'var(--accent-primary)' }}>{webhookUrl}</span>
<LinearCopyButton
textToCopy={webhookUrl}
label="Copy"
label={t('common.copy')}
copiedLabel={t('common.copied')}
iconOnly={true}
className=""
style={{ marginLeft: '6px', padding: '1px 4px', cursor: 'pointer', borderRadius: '3px', border: '1px solid var(--border-color)', background: 'var(--bg-primary)', color: 'var(--text-secondary)', verticalAlign: 'middle' }}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/FileBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ export default function FileBrowser({
{renderBreadcrumbs()}
<div style={{ display: 'flex', gap: '6px', marginLeft: 'auto' }}>
{upload && api.upload && (
<button className="btn btn-secondary" style={{ fontSize: '12px' }} onClick={handleUpload}>⬆ Upload</button>
<button className="btn btn-secondary" style={{ fontSize: '12px' }} onClick={handleUpload}>{t('common.upload', '⬆ Upload')}</button>
)}
{newFolder && (
<button className="btn btn-secondary" style={{ fontSize: '12px' }}
Expand Down
548 changes: 260 additions & 288 deletions frontend/src/i18n/en.json

Large diffs are not rendered by default.

721 changes: 344 additions & 377 deletions frontend/src/i18n/zh.json

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions frontend/src/pages/AdminCompanies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ function PlatformTab() {
// Load email templates
fetchJson<any>('/enterprise/email-templates')
.then(d => {
if (d.templates) setEmailTemplates(d.templates);
if (d.defaults && d.templates) {
setEmailTemplates({ ...d.defaults, ...d.templates });
}
if (d.variables) setEmailTemplateVars(d.variables);
if (d.defaults) setEmailTemplateDefaults(d.defaults);
})
Expand All @@ -196,7 +198,7 @@ function PlatformTab() {
try {
await adminApi.updatePlatformSettings({ [key]: value });
setSettings((s: any) => ({ ...s, [key]: value }));
showToast('Setting updated');
showToast(t('admin.settingUpdated', 'Setting updated'));
} catch (e: any) {
showToast(e.message || 'Failed', 'error');
}
Expand Down Expand Up @@ -773,17 +775,17 @@ function CompaniesTab() {

const columns: { key: SortKey; label: string; flex: string }[] = [
{ key: 'name', label: t('admin.company', 'Company'), flex: '2fr' },
{ key: 'sso_enabled', label: 'SSO', flex: '100px' },
{ key: 'org_admin_email', label: t('admin.orgAdmin', 'Admin Email'), flex: '1.5fr' },
{ key: 'sso_enabled', label: t('admin.sso', 'SSO'), flex: '100px' },
{ key: 'org_admin_email', label: t('admin.orgAdmin', 'Admin Email'), flex: '2fr' },
{ key: 'user_count', label: t('admin.users', 'Users'), flex: '70px' },
{ key: 'agent_count', label: t('admin.agents', 'Agents'), flex: '70px' },
{ key: 'total_tokens', label: t('admin.tokens', 'Token Usage'), flex: '100px' },
{ key: 'agent_count', label: t('admin.agents', 'Agents'), flex: '100px' },
{ key: 'total_tokens', label: t('admin.tokens', 'Token Usage'), flex: '120px' },
{ key: 'created_at', label: t('admin.createdAt', 'Created'), flex: '100px' },
{ key: 'is_active', label: t('admin.status', 'Status'), flex: '100px' },
];
const statusColFlex = '100px';
const actionColFlex = '80px';

const gridCols = columns.map(c => c.flex).join(' ') + ' ' + actionColFlex;
const gridCols = columns.map(c => c.flex).join(' ') + ' ' + statusColFlex + ' ' + actionColFlex;

return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
Expand Down
32 changes: 18 additions & 14 deletions frontend/src/pages/AgentCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ For humans, the message is delivered via their available channel (e.g. Feishu).`
className="btn btn-ghost"
style={{ position: 'absolute', top: '4px', right: '4px', fontSize: '11px', minWidth: '60px' }}
textToCopy={`Please create a skill file at skills/clawith_sync.md with the following content, then add "Check Clawith inbox using the clawith_sync skill and process any pending messages" to your HEARTBEAT.md:\n\n---\nname: clawith_sync\ndescription: Sync with Clawith platform — check inbox, submit results, and send messages.\n---\n\n# Clawith Sync\n\n## When to use\nCheck for new messages from the Clawith platform during every heartbeat cycle.\nYou can also proactively send messages to people and agents in your relationships.\n\n## Instructions\n\n### 1. Check inbox\nMake an HTTP GET request:\n- URL: ${window.location.origin}/api/gateway/poll\n- Header: X-Api-Key: ${createdApiKey}\n\nThe response contains a \`messages\` array. Each message includes:\n- \`id\` — unique message ID (use this for reporting)\n- \`content\` — the message text\n- \`sender_user_name\` — name of the Clawith user who sent it\n- \`sender_user_id\` — unique ID of the sender\n- \`conversation_id\` — the conversation this message belongs to\n- \`history\` — array of previous messages in this conversation for context\n\nThe response also contains a \`relationships\` array describing your colleagues:\n- \`name\` — the person or agent name\n- \`type\` — "human" or "agent"\n- \`role\` — relationship type (e.g. collaborator, supervisor)\n- \`channels\` — available communication channels (e.g. ["feishu"], ["agent"])\n\n**IMPORTANT**: Use the \`history\` array to understand conversation context before replying.\nDifferent \`sender_user_name\` values mean different people — address them accordingly.\n\n### 2. Report results\nFor each completed message, make an HTTP POST request:\n- URL: ${window.location.origin}/api/gateway/report\n- Header: X-Api-Key: ${createdApiKey}\n- Header: Content-Type: application/json\n- Body: {"message_id": "<id from the message>", "result": "<your response>"}\n\n### 3. Send a message to someone\nTo proactively contact a person or agent, make an HTTP POST request:\n- URL: ${window.location.origin}/api/gateway/send-message\n- Header: X-Api-Key: ${createdApiKey}\n- Header: Content-Type: application/json\n- Body: {"target": "<name of person or agent>", "content": "<your message>"}\n\nThe system auto-detects the best channel. For agents, the reply appears in your next poll.\nFor humans, the message is delivered via their available channel (e.g. Feishu).`}
label={t('common.copy', 'Copy')}
copiedLabel="Copied"
label={t('common.copy')}
copiedLabel={t('common.copied')}
/>
</div>
</div>
Expand All @@ -380,8 +380,8 @@ For humans, the message is delivered via their available channel (e.g. Feishu).`
className="btn btn-secondary"
style={{ fontSize: '11px', padding: '4px 12px', minWidth: '70px', height: 'fit-content' }}
textToCopy={createdApiKey}
label={t('common.copy', 'Copy')}
copiedLabel="Copied"
label={t('common.copy')}
copiedLabel={t('common.copied')}
/>
</div>
<p style={{ fontSize: '11px', color: 'var(--text-tertiary)', marginTop: '6px' }}>
Expand Down Expand Up @@ -461,7 +461,7 @@ For humans, the message is delivered via their available channel (e.g. Feishu).`
</p>

<div className="form-group">
<label className="form-label">{t('agent.fields.name')} *</label>
<label className="form-label">{t('agent.fields.name')} <span style={{ color: 'var(--error)' }}>*</span></label>
<input className={`form-input${fieldErrors.name ? ' input-error' : ''}`} value={form.name}
onChange={(e) => { setForm({ ...form, name: e.target.value }); clearFieldError('name'); }}
placeholder={t('openclaw.namePlaceholder', 'e.g. My OpenClaw Bot')} autoFocus />
Expand Down Expand Up @@ -569,14 +569,16 @@ For humans, the message is delivered via their available channel (e.g. Feishu).`
<div
key={tmpl.id}
onClick={() => {
// Parse soul_template to extract personality and boundaries
const sections = parseSoulTemplate(tmpl.soul_template, ['Personality', 'Boundaries']);
const templateData = t(`wizard.templates.templateData.${tmpl.name}`, { returnObjects: true }) as any;
const description = templateData?.description || tmpl.description;
const personality = templateData?.personality || '';
const boundaries = templateData?.boundaries || '';
setForm({
...form,
template_id: tmpl.id,
role_description: tmpl.description,
personality: sections.personality || '',
boundaries: sections.boundaries || '',
role_description: description,
personality: personality,
boundaries: boundaries,
});
}}
style={{
Expand Down Expand Up @@ -714,6 +716,8 @@ For humans, the message is delivered via their available channel (e.g. Feishu).`
{globalSkills.map((skill: any) => {
const isDefault = skill.is_default;
const isChecked = form.skill_ids.includes(skill.id);
const skillName = String(t(`wizard.step3.skills.${skill.name}.name`, skill.name));
const skillDesc = String(t(`wizard.step3.skills.${skill.name}.description`, skill.description));
return (
<label key={skill.id} style={{
display: 'flex', alignItems: 'center', gap: '12px', padding: '12px',
Expand All @@ -737,16 +741,16 @@ For humans, the message is delivered via their available channel (e.g. Feishu).`
<div style={{ fontSize: '18px' }}>{skill.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontWeight: 500, fontSize: '13px' }}>{skill.name}</span>
{isDefault && <span style={{ fontSize: '10px', padding: '1px 6px', borderRadius: '4px', background: 'var(--accent-primary)', color: '#fff', fontWeight: 500 }}>Required</span>}
<span style={{ fontWeight: 500, fontSize: '13px' }}>{skillName}</span>
{isDefault && <span style={{ fontSize: '10px', padding: '1px 6px', borderRadius: '4px', background: 'var(--accent-primary)', color: '#fff', fontWeight: 500 }}>{t('wizard.step3.required')}</span>}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-tertiary)' }}>{skill.description}</div>
<div style={{ fontSize: '11px', color: 'var(--text-tertiary)' }}>{skillDesc}</div>
</div>
</label>);
})}
{globalSkills.length === 0 && (
<div style={{ padding: '16px', background: 'var(--bg-elevated)', borderRadius: '8px', fontSize: '13px', color: 'var(--text-tertiary)', textAlign: 'center' }}>
No skills available. Add skills in Company Settings.
{t('wizard.step3.noSkills')}
</div>
)}
</div>
Expand Down
Loading