feat(deskclaw): rebrand + vibe presets + chat model picker

This commit is contained in:
DeskClaw Bot
2026-04-21 13:56:26 +00:00
Unverified
parent 92144ab639
commit aa4d1fe2b2
23 changed files with 377 additions and 73 deletions

47
DESKCLAW_FEATURES.md Normal file
View File

@@ -0,0 +1,47 @@
# DeskClaw (ClawX fork) — Feature Map
This fork targets “AutoClaw-class” desktop agent UX while staying fully provider-agnostic (BYOK keys, OpenClaw runtime).
## Core
- Chat UI with streaming, markdown, images, tool cards: present (ClawX)
- Thinking visibility: present (Thinking blocks + Execution Graph)
- Vibe coding presets: added (Chat composer chips)
- Live model selection in chat UI:
- Default model selector: added (writes OpenClaw config defaults)
- Per-agent override selector: added
## Agents / Sessions
- Multi-agent management + per-agent workspace: present (ClawX)
- Session list + labels + history: present (ClawX)
- Sub-agent transcript viewing: present (ClawX)
## Skills / Tools
- Preinstalled skills bundling: present (ClawX resources/skills + bundling scripts)
- Skill marketplace + enable/disable: present (ClawX)
## Channels
- Multi-channel bot integrations (Feishu/Lark, Telegram, Discord, WhatsApp, etc.): present (ClawX)
- Multi-account per channel + binding to agents: present (ClawX)
## Automation
- Cron job scheduling + delivery routing: present (ClawX)
## Providers
- Multi-provider UI + secure key storage: present (ClawX)
- Custom OpenAI-compatible endpoints + validation: present (ClawX)
## System / Packaging
- Updater integration (electron-updater): present (ClawX)
- Windows NSIS / macOS dmg / Linux AppImage+deb+rpm packaging: present (ClawX)
## Notes
- “Android” is not supported by Electron. A mobile DeskClaw companion would be a separate project (React Native / Flutter) talking to a remote OpenClaw gateway.

View File

@@ -1,6 +1,6 @@
appId: app.clawx.desktop
productName: ClawX
copyright: Copyright © 2026 ClawX
appId: app.deskclaw.desktop
productName: DeskClaw
copyright: Copyright © 2026 DeskClaw
compression: maximum
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
@@ -59,7 +59,7 @@ publish:
useMultipleRangeRequest: false
- provider: github
owner: ValueCell-ai
repo: ClawX
repo: DeskClaw
# macOS Configuration
mac:
@@ -133,8 +133,8 @@ nsis:
differentialPackage: true
createDesktopShortcut: true
createStartMenuShortcut: true
shortcutName: ClawX
uninstallDisplayName: ClawX
shortcutName: DeskClaw
uninstallDisplayName: DeskClaw
license: LICENSE
include: scripts/installer.nsh
installerIcon: resources/icons/icon.ico
@@ -161,17 +161,17 @@ linux:
arch:
- x64
category: Utility
maintainer: ClawX Team <public@valuecell.ai>
vendor: ClawX
maintainer: DeskClaw Team <public@valuecell.ai>
vendor: DeskClaw
synopsis: AI Assistant powered by OpenClaw
description: ClawX is a graphical AI assistant application that integrates with OpenClaw Gateway to provide intelligent automation and assistance across multiple messaging platforms.
desktop:
entry:
Name: ClawX
Name: DeskClaw
Comment: AI Assistant powered by OpenClaw
Categories: Utility;Network;
Keywords: ai;assistant;automation;chat;
StartupWMClass: clawx
StartupWMClass: deskclaw
appImage:
license: LICENSE

View File

@@ -8,6 +8,7 @@ import {
removeAgentWorkspaceDirectory,
resolveAccountIdForAgent,
updateAgentModel,
updateDefaultModel,
updateAgentName,
} from '../../utils/agent-config';
import { deleteChannelAccountConfig } from '../../utils/channel-config';
@@ -135,6 +136,23 @@ export async function handleAgentRoutes(
return true;
}
if (url.pathname === '/api/agents/default-model' && req.method === 'PUT') {
try {
const body = await parseJsonBody<{ modelRef?: string | null }>(req);
const snapshot = await updateDefaultModel(body.modelRef ?? null);
try {
await syncAllProviderAuthToRuntime();
} catch (syncError) {
console.warn('[agents] Failed to sync runtime after updating default model:', syncError);
}
scheduleGatewayReload(ctx, 'update-default-model');
sendJson(res, 200, { success: true, ...snapshot });
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}
if (url.pathname.startsWith('/api/agents/') && req.method === 'PUT') {
const suffix = url.pathname.slice('/api/agents/'.length);
const parts = suffix.split('/').filter(Boolean);

View File

@@ -48,7 +48,7 @@ import { browserOAuthManager } from '../utils/browser-oauth';
import { whatsAppLoginManager } from '../utils/whatsapp-login';
import { syncAllProviderAuthToRuntime } from '../services/providers/provider-runtime-sync';
const WINDOWS_APP_USER_MODEL_ID = 'app.clawx.desktop';
const WINDOWS_APP_USER_MODEL_ID = 'app.deskclaw.desktop';
const isE2EMode = process.env.CLAWX_E2E === '1';
const requestedUserDataDir = process.env.CLAWX_USER_DATA_DIR?.trim();
@@ -77,7 +77,7 @@ app.disableHardwareAcceleration();
// on X11 it supplements the StartupWMClass matching.
// Must be called before app.whenReady() / before any window is created.
if (process.platform === 'linux') {
app.setDesktopName('clawx.desktop');
app.setDesktopName('deskclaw.desktop');
}
// Prevent multiple instances of the app from running simultaneously.
@@ -96,7 +96,7 @@ if (gotElectronLock && !isE2EMode) {
try {
const fileLock = acquireProcessInstanceFileLock({
userDataDir: app.getPath('userData'),
lockName: 'clawx',
lockName: 'deskclaw',
force: true, // Electron lock already guarantees exclusivity; force-clean orphan/recycled-PID locks
});
gotFileLock = fileLock.acquired;
@@ -281,7 +281,7 @@ function createMainWindow(): BrowserWindow {
async function initialize(): Promise<void> {
// Initialize logger first
logger.init();
logger.info('=== ClawX Application Starting ===');
logger.info('=== DeskClaw Application Starting ===');
logger.debug(
`Runtime: platform=${process.platform}/${process.arch}, electron=${process.versions.electron}, node=${process.versions.node}, packaged=${app.isPackaged}, pid=${process.pid}, ppid=${process.ppid}`
);

View File

@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
import { logger } from '../utils/logger';
import { getSetting } from '../utils/store';
const LINUX_AUTOSTART_FILE = join('.config', 'autostart', 'clawx.desktop');
const LINUX_AUTOSTART_FILE = join('.config', 'autostart', 'deskclaw.desktop');
function quoteDesktopArg(value: string): string {
if (!value) return '""';
@@ -30,8 +30,8 @@ function getLinuxDesktopEntry(): string {
'[Desktop Entry]',
'Type=Application',
'Version=1.0',
'Name=ClawX',
'Comment=ClawX - AI Assistant',
'Name=DeskClaw',
'Comment=DeskClaw - AI Assistant',
`Exec=${getLinuxExecCommand()}`,
'Terminal=false',
'Categories=Utility;',

View File

@@ -176,13 +176,13 @@ export function createMenu(): void {
{
label: 'Documentation',
click: async () => {
await shell.openExternal('https://claw-x.com');
await shell.openExternal('https://github.rommark.dev/admin/DeskClaw');
},
},
{
label: 'Report Issue',
click: async () => {
await shell.openExternal('https://github.com/ValueCell-ai/ClawX/issues');
await shell.openExternal('https://github.rommark.dev/admin/DeskClaw/issues');
},
},
{ type: 'separator' },

View File

@@ -1,7 +1,7 @@
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
const LOCK_SCHEMA = 'clawx-instance-lock';
const LOCK_SCHEMA = 'deskclaw-instance-lock';
const LOCK_VERSION = 1;
export interface ProcessInstanceFileLock {

View File

@@ -57,7 +57,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
tray = new Tray(icon);
// Set tooltip
tray.setToolTip('ClawX - AI Assistant');
tray.setToolTip('DeskClaw - AI Assistant');
const showWindow = () => {
if (mainWindow.isDestroyed()) return;
@@ -68,7 +68,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
// Create context menu
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show ClawX',
label: 'Show DeskClaw',
click: showWindow,
},
{
@@ -122,7 +122,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
type: 'separator',
},
{
label: 'Quit ClawX',
label: 'Quit DeskClaw',
click: () => {
app.quit();
},
@@ -157,7 +157,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
*/
export function updateTrayStatus(status: string): void {
if (tray) {
tray.setToolTip(`ClawX - ${status}`);
tray.setToolTip(`DeskClaw - ${status}`);
}
}

View File

@@ -681,6 +681,31 @@ export async function updateAgentModel(agentId: string, modelRef: string | null)
});
}
export async function updateDefaultModel(modelRef: string | null): Promise<AgentsSnapshot> {
return withConfigLock(async () => {
const config = await readOpenClawConfig() as AgentConfigDocument;
const { agentsConfig } = normalizeAgentsConfig(config);
const normalizedModelRef = typeof modelRef === 'string' ? modelRef.trim() : '';
const nextAgentsConfig: AgentsConfig = { ...(agentsConfig || {}) };
const nextDefaults: AgentDefaultsConfig = { ...(nextAgentsConfig.defaults || {}) };
if (!normalizedModelRef) {
delete nextDefaults.model;
} else {
if (!isValidModelRef(normalizedModelRef)) {
throw new Error('modelRef must be in "provider/model" format');
}
nextDefaults.model = { primary: normalizedModelRef };
}
nextAgentsConfig.defaults = nextDefaults;
config.agents = nextAgentsConfig;
await writeOpenClawConfig(config);
logger.info('Updated default model', { modelRef: normalizedModelRef || null });
return buildSnapshotFromConfig(config);
});
}
export async function deleteAgentConfig(agentId: string): Promise<{ snapshot: AgentsSnapshot; removedEntry: AgentListEntry }> {
return withConfigLock(async () => {
if (agentId === MAIN_AGENT_ID) {

View File

@@ -1,5 +1,5 @@
{
"name": "clawx",
"name": "deskclaw",
"version": "0.3.10",
"pnpm": {
"onlyBuiltDependencies": [
@@ -26,9 +26,9 @@
]
}
},
"description": "ClawX - Graphical AI Assistant based on OpenClaw",
"description": "DeskClaw - Graphical AI Assistant based on OpenClaw",
"main": "dist-electron/main/index.js",
"author": "ClawX Team",
"author": "DeskClaw Team",
"license": "MIT",
"private": true,
"scripts": {

View File

@@ -2,7 +2,7 @@
"gatewayNotRunning": "Gateway Not Running",
"gatewayRequired": "The OpenClaw Gateway needs to be running to use chat. It will start automatically, or you can start it from Settings.",
"welcome": {
"title": "ClawX Chat",
"title": "DeskClaw Chat",
"subtitle": "What can I do for you?",
"askQuestions": "Handle Tasks",
"askQuestionsDesc": "Work on task-oriented requests",
@@ -19,7 +19,7 @@
"eyebrow": "Run View",
"title": "Task Outline",
"emptyTitle": "No structured steps yet",
"emptyBody": "Once a run starts, ClawX will surface thinking, tool calls, and handoff states here.",
"emptyBody": "Once a run starts, DeskClaw will surface thinking, tool calls, and handoff states here.",
"status": {
"idle": "Idle",
"running_one": "1 active step",

View File

@@ -1,6 +1,6 @@
{
"title": "Settings",
"subtitle": "Configure your ClawX experience",
"subtitle": "Configure your DeskClaw experience",
"appearance": {
"title": "General",
"description": "Customize the look and feel",
@@ -10,7 +10,7 @@
"system": "System",
"language": "Language",
"launchAtStartup": "Launch at system startup",
"launchAtStartupDesc": "Automatically launch ClawX when you log in"
"launchAtStartupDesc": "Automatically launch DeskClaw when you log in"
},
"aiProviders": {
"title": "AI Providers",
@@ -37,7 +37,7 @@
"notRequired": "Not required",
"empty": {
"title": "No providers configured",
"desc": "Add an AI provider to start using ClawX",
"desc": "Add an AI provider to start using DeskClaw",
"cta": "Add Your First Provider"
},
"dialog": {
@@ -60,7 +60,7 @@
"protocol": "Protocol",
"advancedConfig": "Advanced configuration",
"userAgent": "User-Agent",
"userAgentPlaceholder": "ClawX/1.0",
"userAgentPlaceholder": "DeskClaw/1.0",
"fallbackModels": "Fallback Models",
"fallbackProviders": "Fallback Providers",
"fallbackModelIds": "Fallback Model IDs",
@@ -134,7 +134,7 @@
"appLogs": "Application Logs",
"openFolder": "Open Folder",
"autoStart": "Auto-start Gateway",
"autoStartDesc": "Start Gateway when ClawX launches",
"autoStartDesc": "Start Gateway when DeskClaw launches",
"proxyTitle": "Proxy",
"proxyDesc": "Route Electron and Gateway traffic through your local proxy client.",
"proxyServer": "Proxy Server",
@@ -155,7 +155,7 @@
},
"updates": {
"title": "Updates",
"description": "Keep ClawX up to date",
"description": "Keep DeskClaw up to date",
"autoCheck": "Auto-check for updates",
"autoCheckDesc": "Check for updates on startup",
"autoDownload": "Auto-update",
@@ -209,7 +209,7 @@
"devMode": "Developer Mode",
"devModeDesc": "Show developer tools and shortcuts",
"telemetry": "Anonymous Usage Data",
"telemetryDesc": "Allow providing anonymous basic usage data to improve ClawX"
"telemetryDesc": "Allow providing anonymous basic usage data to improve DeskClaw"
},
"developer": {
"title": "Developer",
@@ -271,7 +271,7 @@
},
"about": {
"title": "About",
"appName": "ClawX",
"appName": "DeskClaw",
"tagline": "Graphical AI Assistant",
"basedOn": "Based on OpenClaw",
"version": "Version {{version}}",

View File

@@ -1,7 +1,7 @@
{
"steps": {
"welcome": {
"title": "Welcome to ClawX",
"title": "Welcome to DeskClaw",
"description": "Your AI assistant is ready to be configured"
},
"runtime": {
@@ -22,12 +22,12 @@
},
"complete": {
"title": "All Set!",
"description": "ClawX is ready to use"
"description": "DeskClaw is ready to use"
}
},
"welcome": {
"title": "Welcome to ClawX",
"description": "ClawX is a graphical interface for OpenClaw, making it easy to use AI assistants across your favorite messaging platforms.",
"title": "Welcome to DeskClaw",
"description": "DeskClaw is a graphical interface for OpenClaw, making it easy to use AI assistants across your favorite messaging platforms.",
"features": {
"noCommand": "Zero command-line required",
"modernUI": "Modern, beautiful interface",
@@ -113,7 +113,7 @@
},
"complete": {
"title": "Setup Complete!",
"subtitle": "ClawX is configured and ready to use. You can now start chatting with your AI assistant.",
"subtitle": "DeskClaw is configured and ready to use. You can now start chatting with your AI assistant.",
"provider": "AI Provider",
"components": "Components",
"gateway": "Gateway",

View File

@@ -2,7 +2,7 @@
"gatewayNotRunning": "ゲートウェイが停止中",
"gatewayRequired": "チャットを利用するには OpenClaw ゲートウェイが実行されている必要があります。自動的に起動するか、設定から起動できます。",
"welcome": {
"title": "ClawX チャット",
"title": "DeskClaw チャット",
"subtitle": "お手伝いできることはありますか?",
"askQuestions": "タスク対応",
"askQuestionsDesc": "タスク指向の依頼に対応します",
@@ -19,7 +19,7 @@
"eyebrow": "実行ビュー",
"title": "タスクの流れ",
"emptyTitle": "まだ構造化されたステップはありません",
"emptyBody": "実行が始まると、ClawX は思考・ツール呼び出し・最終化の状態をここに表示します。",
"emptyBody": "実行が始まると、DeskClaw は思考・ツール呼び出し・最終化の状態をここに表示します。",
"status": {
"idle": "待機中",
"running_one": "進行中 1 件",

View File

@@ -1,6 +1,6 @@
{
"title": "設定",
"subtitle": "ClawX の体験をカスタマイズ",
"subtitle": "DeskClaw の体験をカスタマイズ",
"appearance": {
"title": "通用",
"description": "外観とスタイルをカスタマイズ",
@@ -10,7 +10,7 @@
"system": "システム",
"language": "言語",
"launchAtStartup": "システム起動時に自動起動",
"launchAtStartupDesc": "ログイン時に ClawX を自動的に起動します"
"launchAtStartupDesc": "ログイン時に DeskClaw を自動的に起動します"
},
"aiProviders": {
"title": "AI プロバイダー",
@@ -37,7 +37,7 @@
"notRequired": "不要",
"empty": {
"title": "プロバイダーが構成されていません",
"desc": "ClawX の使用を開始するには AI プロバイダーを追加してください",
"desc": "DeskClaw の使用を開始するには AI プロバイダーを追加してください",
"cta": "最初のプロバイダーを追加"
},
"dialog": {
@@ -60,7 +60,7 @@
"protocol": "プロトコル",
"advancedConfig": "詳細設定",
"userAgent": "User-Agent",
"userAgentPlaceholder": "ClawX/1.0",
"userAgentPlaceholder": "DeskClaw/1.0",
"fallbackModels": "フォールバックモデル",
"fallbackProviders": "別プロバイダーへのフォールバック",
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
@@ -133,7 +133,7 @@
"appLogs": "アプリケーションログ",
"openFolder": "フォルダーを開く",
"autoStart": "ゲートウェイ自動起動",
"autoStartDesc": "ClawX 起動時にゲートウェイを自動起動",
"autoStartDesc": "DeskClaw 起動時にゲートウェイを自動起動",
"proxyTitle": "プロキシ",
"proxyDesc": "Electron と Gateway の通信をローカルプロキシ経由にします。",
"proxyServer": "プロキシサーバー",
@@ -154,7 +154,7 @@
},
"updates": {
"title": "アップデート",
"description": "ClawX を最新に保つ",
"description": "DeskClaw を最新に保つ",
"autoCheck": "自動更新チェック",
"autoCheckDesc": "起動時に更新を確認",
"autoDownload": "自動アップデート",
@@ -268,7 +268,7 @@
},
"about": {
"title": "バージョン情報",
"appName": "ClawX",
"appName": "DeskClaw",
"tagline": "グラフィカル AI アシスタント",
"basedOn": "OpenClaw ベース",
"version": "バージョン {{version}}",

View File

@@ -1,7 +1,7 @@
{
"steps": {
"welcome": {
"title": "ClawXへようこそ",
"title": "DeskClawへようこそ",
"description": "AIアシスタントの設定準備が整いました"
},
"runtime": {
@@ -22,12 +22,12 @@
},
"complete": {
"title": "完了!",
"description": "ClawXを使用する準備が整いました"
"description": "DeskClawを使用する準備が整いました"
}
},
"welcome": {
"title": "ClawXへようこそ",
"description": "ClawXはOpenClawのグラフィカルインターフェースで、お気に入りのメッセージングプラットフォームでAIアシスタントを簡単に使用できます。",
"title": "DeskClawへようこそ",
"description": "DeskClawはOpenClawのグラフィカルインターフェースで、お気に入りのメッセージングプラットフォームでAIアシスタントを簡単に使用できます。",
"features": {
"noCommand": "コマンドライン不要",
"modernUI": "モダンで美しいインターフェース",

View File

@@ -2,7 +2,7 @@
"gatewayNotRunning": "网关未运行",
"gatewayRequired": "OpenClaw 网关需要运行才能使用聊天。它将自动启动,或者您可以从设置中启动。",
"welcome": {
"title": "ClawX 聊天",
"title": "DeskClaw 聊天",
"subtitle": "我能为你做些什么?",
"askQuestions": "处理任务",
"askQuestionsDesc": "处理面向任务的请求",
@@ -19,7 +19,7 @@
"eyebrow": "运行视图",
"title": "任务脉络",
"emptyTitle": "还没有结构化步骤",
"emptyBody": "当一次运行开始后ClawX 会在这里展示思考、工具调用和收尾状态。",
"emptyBody": "当一次运行开始后,DeskClaw 会在这里展示思考、工具调用和收尾状态。",
"status": {
"idle": "空闲",
"running_one": "1 个活动步骤",

View File

@@ -1,6 +1,6 @@
{
"title": "设置",
"subtitle": "配置您的 ClawX 体验",
"subtitle": "配置您的 DeskClaw 体验",
"appearance": {
"title": "通用",
"description": "自定义外观和风格",
@@ -10,7 +10,7 @@
"system": "跟随系统",
"language": "语言",
"launchAtStartup": "开机自动启动",
"launchAtStartupDesc": "登录系统后自动启动 ClawX"
"launchAtStartupDesc": "登录系统后自动启动 DeskClaw"
},
"aiProviders": {
"title": "AI 模型提供商",
@@ -37,7 +37,7 @@
"notRequired": "非必填",
"empty": {
"title": "未配置提供商",
"desc": "添加 AI 提供商以开始使用 ClawX",
"desc": "添加 AI 提供商以开始使用 DeskClaw",
"cta": "添加您的第一个提供商"
},
"dialog": {
@@ -60,7 +60,7 @@
"protocol": "协议",
"advancedConfig": "高级配置",
"userAgent": "User-Agent",
"userAgentPlaceholder": "ClawX/1.0",
"userAgentPlaceholder": "DeskClaw/1.0",
"fallbackModels": "回退模型",
"fallbackProviders": "跨 Provider 回退",
"fallbackModelIds": "同 Provider 回退模型 ID",
@@ -134,7 +134,7 @@
"appLogs": "应用日志",
"openFolder": "打开文件夹",
"autoStart": "自动启动网关",
"autoStartDesc": "ClawX 启动时自动启动网关",
"autoStartDesc": "DeskClaw 启动时自动启动网关",
"proxyTitle": "代理",
"proxyDesc": "让 Electron 和 Gateway 的网络请求都走本地代理客户端。",
"proxyServer": "代理服务器",
@@ -155,7 +155,7 @@
},
"updates": {
"title": "更新",
"description": "保持 ClawX 最新",
"description": "保持 DeskClaw 最新",
"autoCheck": "自动检查更新",
"autoCheckDesc": "启动时检查更新",
"autoDownload": "自动更新",
@@ -209,7 +209,7 @@
"devMode": "开发者模式",
"devModeDesc": "显示开发者工具和快捷方式",
"telemetry": "匿名使用数据",
"telemetryDesc": "允许提供匿名的基础使用数据,用于改进 ClawX"
"telemetryDesc": "允许提供匿名的基础使用数据,用于改进 DeskClaw"
},
"developer": {
"title": "开发者",
@@ -271,7 +271,7 @@
},
"about": {
"title": "关于",
"appName": "ClawX",
"appName": "DeskClaw",
"tagline": "图形化 AI 助手",
"basedOn": "基于 OpenClaw",
"version": "版本 {{version}}",

View File

@@ -1,7 +1,7 @@
{
"steps": {
"welcome": {
"title": "欢迎使用 ClawX",
"title": "欢迎使用 DeskClaw",
"description": "您的 AI 助手已准备好进行配置"
},
"runtime": {
@@ -22,12 +22,12 @@
},
"complete": {
"title": "准备就绪!",
"description": "ClawX 已准备好使用"
"description": "DeskClaw 已准备好使用"
}
},
"welcome": {
"title": "欢迎使用 ClawX",
"description": "ClawX 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。",
"title": "欢迎使用 DeskClaw",
"description": "DeskClaw 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。",
"features": {
"noCommand": "无需命令行",
"modernUI": "现代美观的界面",

View File

@@ -87,6 +87,7 @@ function readFileAsBase64(file: globalThis.File): Promise<string> {
export function ChatInput({ onSend, onStop, disabled = false, sending = false, isEmpty = false }: ChatInputProps) {
const { t } = useTranslation('chat');
const [input, setInput] = useState('');
const [vibeMode, setVibeMode] = useState<string>('');
const [attachments, setAttachments] = useState<FileAttachment[]>([]);
const [targetAgentId, setTargetAgentId] = useState<string | null>(null);
const [pickerOpen, setPickerOpen] = useState(false);
@@ -109,6 +110,31 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
[agents, targetAgentId],
);
const showAgentPicker = mentionableAgents.length > 0;
const vibePresets = useMemo(
() => [
{
key: 'plan',
label: 'Plan',
text: 'Mode: Plan.\nAsk missing questions first. Then propose a step-by-step plan and a short checklist. Wait for confirmation before implementing.',
},
{
key: 'build',
label: 'Build',
text: 'Mode: Build.\nImplement the requested change. Keep it minimal, production-grade, and include verification steps.',
},
{
key: 'debug',
label: 'Debug',
text: 'Mode: Debug.\nList hypotheses, propose the smallest reproduction, add instrumentation, then fix the root cause.',
},
{
key: 'review',
label: 'Review',
text: 'Mode: Review.\nReview for correctness, security, and edge cases. Propose concrete fixes.',
},
],
[],
);
// Auto-resize textarea
useEffect(() => {
@@ -293,7 +319,11 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
const readyAttachments = attachments.filter(a => a.status === 'ready');
// Capture values before clearing — clear input immediately for snappy UX,
// but keep attachments available for the async send
const textToSend = input.trim();
let textToSend = input.trim();
const preset = vibeMode ? vibePresets.find((p) => p.key === vibeMode) : null;
if (preset) {
textToSend = textToSend ? `${preset.text}\n\n---\n\n${textToSend}` : preset.text;
}
const attachmentsToSend = readyAttachments.length > 0 ? readyAttachments : undefined;
console.log(`[handleSend] text="${textToSend.substring(0, 50)}", attachments=${attachments.length}, ready=${readyAttachments.length}, sending=${!!attachmentsToSend}`);
if (attachmentsToSend) {
@@ -310,7 +340,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
onSend(textToSend, attachmentsToSend, targetAgentId);
setTargetAgentId(null);
setPickerOpen(false);
}, [input, attachments, canSend, onSend, targetAgentId]);
}, [input, attachments, canSend, onSend, targetAgentId, vibeMode, vibePresets]);
const handleStop = useCallback(() => {
if (!canStop) return;
@@ -423,6 +453,40 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
</div>
)}
{!disabled && (
<div className="flex flex-wrap gap-1 pb-1.5">
<button
type="button"
className={cn(
'rounded-full border px-2.5 py-1 text-[12px] font-medium transition-colors',
vibeMode === ''
? 'border-primary/30 bg-primary/10 text-primary'
: 'border-black/10 bg-black/[0.03] text-muted-foreground hover:bg-black/5 hover:text-foreground dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/[0.06]'
)}
onClick={() => setVibeMode('')}
disabled={sending}
>
Normal
</button>
{vibePresets.map((preset) => (
<button
key={preset.key}
type="button"
className={cn(
'rounded-full border px-2.5 py-1 text-[12px] font-medium transition-colors',
vibeMode === preset.key
? 'border-primary/30 bg-primary/10 text-primary'
: 'border-black/10 bg-black/[0.03] text-muted-foreground hover:bg-black/5 hover:text-foreground dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/[0.06]'
)}
onClick={() => setVibeMode(preset.key)}
disabled={sending}
>
{preset.label}
</button>
))}
</div>
)}
{/* Text Row — flush-left */}
<Textarea
ref={textareaRef}

View File

@@ -6,22 +6,118 @@
import { useMemo } from 'react';
import { RefreshCw, Bot } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useChatStore } from '@/stores/chat';
import { useAgentsStore } from '@/stores/agents';
import { useProviderStore } from '@/stores/providers';
import type { ProviderAccount, ProviderVendorInfo, ProviderWithKeyInfo } from '@/stores/providers';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
function providerKeyFromAccount(account: ProviderAccount): string {
if (account.vendorId === 'custom' || account.vendorId === 'ollama') {
const suffix = account.id.replace(/-/g, '').slice(0, 8);
return `${account.vendorId}-${suffix}`;
}
if (account.vendorId === 'minimax-portal-cn') {
return 'minimax-portal';
}
return account.vendorId;
}
function hasConfiguredProviderCredentials(
account: ProviderAccount,
statusById: Map<string, ProviderWithKeyInfo>,
): boolean {
if (account.authMode === 'oauth_device' || account.authMode === 'oauth_browser' || account.authMode === 'local') {
return true;
}
return statusById.get(account.id)?.hasKey ?? false;
}
function resolveAccountDefaultModelId(account: ProviderAccount, vendor: ProviderVendorInfo | undefined): string | undefined {
const fromAccount = (account.model || '').trim();
if (fromAccount) return fromAccount;
const fromVendor = (vendor?.defaultModelId || '').trim();
return fromVendor || undefined;
}
function splitModelRef(modelRef: string | null | undefined): { providerKey: string; modelId: string } | null {
const value = (modelRef || '').trim();
if (!value) return null;
const separatorIndex = value.indexOf('/');
if (separatorIndex <= 0 || separatorIndex >= value.length - 1) return null;
return {
providerKey: value.slice(0, separatorIndex),
modelId: value.slice(separatorIndex + 1),
};
}
export function ChatToolbar() {
const refresh = useChatStore((s) => s.refresh);
const loading = useChatStore((s) => s.loading);
const currentAgentId = useChatStore((s) => s.currentAgentId);
const agents = useAgentsStore((s) => s.agents);
const defaultModelRef = useAgentsStore((s) => s.defaultModelRef);
const updateDefaultModel = useAgentsStore((s) => s.updateDefaultModel);
const updateAgentModel = useAgentsStore((s) => s.updateAgentModel);
const providerAccounts = useProviderStore((s) => s.accounts);
const providerStatuses = useProviderStore((s) => s.statuses);
const providerVendors = useProviderStore((s) => s.vendors);
const { t } = useTranslation('chat');
const currentAgentName = useMemo(
() => (agents ?? []).find((agent) => agent.id === currentAgentId)?.name ?? currentAgentId,
[agents, currentAgentId],
);
const currentAgent = useMemo(
() => (agents ?? []).find((agent) => agent.id === currentAgentId) ?? null,
[agents, currentAgentId],
);
const providerVendorById = useMemo(() => {
const map = new Map<string, ProviderVendorInfo>();
for (const v of providerVendors) map.set(v.id, v);
return map;
}, [providerVendors]);
const providerStatusById = useMemo(() => {
const map = new Map<string, ProviderWithKeyInfo>();
for (const s of providerStatuses) map.set(s.id, s);
return map;
}, [providerStatuses]);
const runtimeModelOptions = useMemo(() => {
return (providerAccounts ?? [])
.filter((a) => a.enabled)
.filter((a) => hasConfiguredProviderCredentials(a, providerStatusById))
.map((account) => {
const vendor = providerVendorById.get(account.vendorId);
const providerKey = providerKeyFromAccount(account);
const modelId = resolveAccountDefaultModelId(account, vendor);
const modelRef = modelId ? `${providerKey}/${modelId}` : null;
const label = `${account.label || providerKey}${modelId ? ` · ${modelId}` : ''}`;
return {
id: account.id,
providerKey,
modelId,
modelRef,
label,
};
})
.filter((o) => Boolean(o.modelRef));
}, [providerAccounts, providerStatusById, providerVendorById]);
const defaultModelSelectValue = useMemo(() => {
const parsed = splitModelRef(defaultModelRef);
if (!parsed) return '';
return defaultModelRef || '';
}, [defaultModelRef]);
const agentOverrideValue = useMemo(() => {
const override = currentAgent?.overrideModelRef;
return override || '';
}, [currentAgent?.overrideModelRef]);
return (
<div className="flex items-center gap-2">
@@ -29,6 +125,42 @@ export function ChatToolbar() {
<Bot className="h-3.5 w-3.5 text-primary" />
<span>{t('toolbar.currentAgent', { agent: currentAgentName })}</span>
</div>
<div className="hidden md:flex items-center gap-2">
<Select
className="h-8 w-[260px] text-[12px]"
value={defaultModelSelectValue}
onChange={(e) => {
const next = e.target.value.trim();
void updateDefaultModel(next ? next : null);
}}
disabled={runtimeModelOptions.length === 0}
title="Default model"
>
<option value="">{t('toolbar.modelDefaultUnset', { defaultValue: 'Default model (not set)' })}</option>
{runtimeModelOptions.map((o) => (
<option key={o.id} value={o.modelRef || ''}>
{o.label}
</option>
))}
</Select>
<Select
className="h-8 w-[260px] text-[12px]"
value={agentOverrideValue}
onChange={(e) => {
const next = e.target.value.trim();
void updateAgentModel(currentAgentId, next ? next : null);
}}
disabled={!currentAgent || runtimeModelOptions.length === 0}
title="Agent override"
>
<option value="">{t('toolbar.modelInherit', { defaultValue: 'Inherit default' })}</option>
{runtimeModelOptions.map((o) => (
<option key={o.id} value={o.modelRef || ''}>
{o.label}
</option>
))}
</Select>
</div>
{/* Refresh */}
<Tooltip>
<TooltipTrigger asChild>

View File

@@ -1091,14 +1091,14 @@ export function Settings() {
<Button
variant="link"
className="h-auto p-0 text-[14px] text-blue-500 hover:text-blue-600 font-medium"
onClick={() => window.electron.openExternal('https://claw-x.com')}
onClick={() => window.electron.openExternal('https://github.rommark.dev/admin/DeskClaw')}
>
{t('about.docs')}
</Button>
<Button
variant="link"
className="h-auto p-0 text-[14px] text-blue-500 hover:text-blue-600 font-medium"
onClick={() => window.electron.openExternal('https://github.com/ValueCell-ai/ClawX')}
onClick={() => window.electron.openExternal('https://github.rommark.dev/admin/DeskClaw')}
>
{t('about.github')}
</Button>

View File

@@ -16,6 +16,7 @@ interface AgentsState {
createAgent: (name: string, options?: { inheritWorkspace?: boolean }) => Promise<void>;
updateAgent: (agentId: string, name: string) => Promise<void>;
updateAgentModel: (agentId: string, modelRef: string | null) => Promise<void>;
updateDefaultModel: (modelRef: string | null) => Promise<void>;
deleteAgent: (agentId: string) => Promise<void>;
assignChannel: (agentId: string, channelType: ChannelType) => Promise<void>;
removeChannel: (agentId: string, channelType: ChannelType) => Promise<void>;
@@ -104,6 +105,23 @@ export const useAgentsStore = create<AgentsState>((set) => ({
}
},
updateDefaultModel: async (modelRef: string | null) => {
set({ error: null });
try {
const snapshot = await hostApiFetch<AgentsSnapshot & { success?: boolean }>(
'/api/agents/default-model',
{
method: 'PUT',
body: JSON.stringify({ modelRef }),
}
);
set(applySnapshot(snapshot));
} catch (error) {
set({ error: String(error) });
throw error;
}
},
deleteAgent: async (agentId: string) => {
set({ error: null });
try {