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

View File

@@ -8,6 +8,7 @@ import {
removeAgentWorkspaceDirectory, removeAgentWorkspaceDirectory,
resolveAccountIdForAgent, resolveAccountIdForAgent,
updateAgentModel, updateAgentModel,
updateDefaultModel,
updateAgentName, updateAgentName,
} from '../../utils/agent-config'; } from '../../utils/agent-config';
import { deleteChannelAccountConfig } from '../../utils/channel-config'; import { deleteChannelAccountConfig } from '../../utils/channel-config';
@@ -135,6 +136,23 @@ export async function handleAgentRoutes(
return true; 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') { if (url.pathname.startsWith('/api/agents/') && req.method === 'PUT') {
const suffix = url.pathname.slice('/api/agents/'.length); const suffix = url.pathname.slice('/api/agents/'.length);
const parts = suffix.split('/').filter(Boolean); 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 { whatsAppLoginManager } from '../utils/whatsapp-login';
import { syncAllProviderAuthToRuntime } from '../services/providers/provider-runtime-sync'; 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 isE2EMode = process.env.CLAWX_E2E === '1';
const requestedUserDataDir = process.env.CLAWX_USER_DATA_DIR?.trim(); const requestedUserDataDir = process.env.CLAWX_USER_DATA_DIR?.trim();
@@ -77,7 +77,7 @@ app.disableHardwareAcceleration();
// on X11 it supplements the StartupWMClass matching. // on X11 it supplements the StartupWMClass matching.
// Must be called before app.whenReady() / before any window is created. // Must be called before app.whenReady() / before any window is created.
if (process.platform === 'linux') { if (process.platform === 'linux') {
app.setDesktopName('clawx.desktop'); app.setDesktopName('deskclaw.desktop');
} }
// Prevent multiple instances of the app from running simultaneously. // Prevent multiple instances of the app from running simultaneously.
@@ -96,7 +96,7 @@ if (gotElectronLock && !isE2EMode) {
try { try {
const fileLock = acquireProcessInstanceFileLock({ const fileLock = acquireProcessInstanceFileLock({
userDataDir: app.getPath('userData'), userDataDir: app.getPath('userData'),
lockName: 'clawx', lockName: 'deskclaw',
force: true, // Electron lock already guarantees exclusivity; force-clean orphan/recycled-PID locks force: true, // Electron lock already guarantees exclusivity; force-clean orphan/recycled-PID locks
}); });
gotFileLock = fileLock.acquired; gotFileLock = fileLock.acquired;
@@ -281,7 +281,7 @@ function createMainWindow(): BrowserWindow {
async function initialize(): Promise<void> { async function initialize(): Promise<void> {
// Initialize logger first // Initialize logger first
logger.init(); logger.init();
logger.info('=== ClawX Application Starting ==='); logger.info('=== DeskClaw Application Starting ===');
logger.debug( 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}` `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 { logger } from '../utils/logger';
import { getSetting } from '../utils/store'; 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 { function quoteDesktopArg(value: string): string {
if (!value) return '""'; if (!value) return '""';
@@ -30,8 +30,8 @@ function getLinuxDesktopEntry(): string {
'[Desktop Entry]', '[Desktop Entry]',
'Type=Application', 'Type=Application',
'Version=1.0', 'Version=1.0',
'Name=ClawX', 'Name=DeskClaw',
'Comment=ClawX - AI Assistant', 'Comment=DeskClaw - AI Assistant',
`Exec=${getLinuxExecCommand()}`, `Exec=${getLinuxExecCommand()}`,
'Terminal=false', 'Terminal=false',
'Categories=Utility;', 'Categories=Utility;',

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
tray = new Tray(icon); tray = new Tray(icon);
// Set tooltip // Set tooltip
tray.setToolTip('ClawX - AI Assistant'); tray.setToolTip('DeskClaw - AI Assistant');
const showWindow = () => { const showWindow = () => {
if (mainWindow.isDestroyed()) return; if (mainWindow.isDestroyed()) return;
@@ -68,7 +68,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
// Create context menu // Create context menu
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: 'Show ClawX', label: 'Show DeskClaw',
click: showWindow, click: showWindow,
}, },
{ {
@@ -122,7 +122,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
type: 'separator', type: 'separator',
}, },
{ {
label: 'Quit ClawX', label: 'Quit DeskClaw',
click: () => { click: () => {
app.quit(); app.quit();
}, },
@@ -157,7 +157,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
*/ */
export function updateTrayStatus(status: string): void { export function updateTrayStatus(status: string): void {
if (tray) { 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 }> { export async function deleteAgentConfig(agentId: string): Promise<{ snapshot: AgentsSnapshot; removedEntry: AgentListEntry }> {
return withConfigLock(async () => { return withConfigLock(async () => {
if (agentId === MAIN_AGENT_ID) { if (agentId === MAIN_AGENT_ID) {

View File

@@ -1,5 +1,5 @@
{ {
"name": "clawx", "name": "deskclaw",
"version": "0.3.10", "version": "0.3.10",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "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", "main": "dist-electron/main/index.js",
"author": "ClawX Team", "author": "DeskClaw Team",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -2,7 +2,7 @@
"gatewayNotRunning": "Gateway Not Running", "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.", "gatewayRequired": "The OpenClaw Gateway needs to be running to use chat. It will start automatically, or you can start it from Settings.",
"welcome": { "welcome": {
"title": "ClawX Chat", "title": "DeskClaw Chat",
"subtitle": "What can I do for you?", "subtitle": "What can I do for you?",
"askQuestions": "Handle Tasks", "askQuestions": "Handle Tasks",
"askQuestionsDesc": "Work on task-oriented requests", "askQuestionsDesc": "Work on task-oriented requests",
@@ -19,7 +19,7 @@
"eyebrow": "Run View", "eyebrow": "Run View",
"title": "Task Outline", "title": "Task Outline",
"emptyTitle": "No structured steps yet", "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": { "status": {
"idle": "Idle", "idle": "Idle",
"running_one": "1 active step", "running_one": "1 active step",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"steps": { "steps": {
"welcome": { "welcome": {
"title": "欢迎使用 ClawX", "title": "欢迎使用 DeskClaw",
"description": "您的 AI 助手已准备好进行配置" "description": "您的 AI 助手已准备好进行配置"
}, },
"runtime": { "runtime": {
@@ -22,12 +22,12 @@
}, },
"complete": { "complete": {
"title": "准备就绪!", "title": "准备就绪!",
"description": "ClawX 已准备好使用" "description": "DeskClaw 已准备好使用"
} }
}, },
"welcome": { "welcome": {
"title": "欢迎使用 ClawX", "title": "欢迎使用 DeskClaw",
"description": "ClawX 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。", "description": "DeskClaw 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。",
"features": { "features": {
"noCommand": "无需命令行", "noCommand": "无需命令行",
"modernUI": "现代美观的界面", "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) { export function ChatInput({ onSend, onStop, disabled = false, sending = false, isEmpty = false }: ChatInputProps) {
const { t } = useTranslation('chat'); const { t } = useTranslation('chat');
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [vibeMode, setVibeMode] = useState<string>('');
const [attachments, setAttachments] = useState<FileAttachment[]>([]); const [attachments, setAttachments] = useState<FileAttachment[]>([]);
const [targetAgentId, setTargetAgentId] = useState<string | null>(null); const [targetAgentId, setTargetAgentId] = useState<string | null>(null);
const [pickerOpen, setPickerOpen] = useState(false); const [pickerOpen, setPickerOpen] = useState(false);
@@ -109,6 +110,31 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
[agents, targetAgentId], [agents, targetAgentId],
); );
const showAgentPicker = mentionableAgents.length > 0; 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 // Auto-resize textarea
useEffect(() => { useEffect(() => {
@@ -293,7 +319,11 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
const readyAttachments = attachments.filter(a => a.status === 'ready'); const readyAttachments = attachments.filter(a => a.status === 'ready');
// Capture values before clearing — clear input immediately for snappy UX, // Capture values before clearing — clear input immediately for snappy UX,
// but keep attachments available for the async send // 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; const attachmentsToSend = readyAttachments.length > 0 ? readyAttachments : undefined;
console.log(`[handleSend] text="${textToSend.substring(0, 50)}", attachments=${attachments.length}, ready=${readyAttachments.length}, sending=${!!attachmentsToSend}`); console.log(`[handleSend] text="${textToSend.substring(0, 50)}", attachments=${attachments.length}, ready=${readyAttachments.length}, sending=${!!attachmentsToSend}`);
if (attachmentsToSend) { if (attachmentsToSend) {
@@ -310,7 +340,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
onSend(textToSend, attachmentsToSend, targetAgentId); onSend(textToSend, attachmentsToSend, targetAgentId);
setTargetAgentId(null); setTargetAgentId(null);
setPickerOpen(false); setPickerOpen(false);
}, [input, attachments, canSend, onSend, targetAgentId]); }, [input, attachments, canSend, onSend, targetAgentId, vibeMode, vibePresets]);
const handleStop = useCallback(() => { const handleStop = useCallback(() => {
if (!canStop) return; if (!canStop) return;
@@ -423,6 +453,40 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
</div> </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 */} {/* Text Row — flush-left */}
<Textarea <Textarea
ref={textareaRef} ref={textareaRef}

View File

@@ -6,22 +6,118 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { RefreshCw, Bot } from 'lucide-react'; import { RefreshCw, Bot } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useChatStore } from '@/stores/chat'; import { useChatStore } from '@/stores/chat';
import { useAgentsStore } from '@/stores/agents'; import { useAgentsStore } from '@/stores/agents';
import { useProviderStore } from '@/stores/providers';
import type { ProviderAccount, ProviderVendorInfo, ProviderWithKeyInfo } from '@/stores/providers';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next'; 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() { export function ChatToolbar() {
const refresh = useChatStore((s) => s.refresh); const refresh = useChatStore((s) => s.refresh);
const loading = useChatStore((s) => s.loading); const loading = useChatStore((s) => s.loading);
const currentAgentId = useChatStore((s) => s.currentAgentId); const currentAgentId = useChatStore((s) => s.currentAgentId);
const agents = useAgentsStore((s) => s.agents); 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 { t } = useTranslation('chat');
const currentAgentName = useMemo( const currentAgentName = useMemo(
() => (agents ?? []).find((agent) => agent.id === currentAgentId)?.name ?? currentAgentId, () => (agents ?? []).find((agent) => agent.id === currentAgentId)?.name ?? currentAgentId,
[agents, 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 ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -29,6 +125,42 @@ export function ChatToolbar() {
<Bot className="h-3.5 w-3.5 text-primary" /> <Bot className="h-3.5 w-3.5 text-primary" />
<span>{t('toolbar.currentAgent', { agent: currentAgentName })}</span> <span>{t('toolbar.currentAgent', { agent: currentAgentName })}</span>
</div> </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 */} {/* Refresh */}
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>

View File

@@ -1091,14 +1091,14 @@ export function Settings() {
<Button <Button
variant="link" variant="link"
className="h-auto p-0 text-[14px] text-blue-500 hover:text-blue-600 font-medium" 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')} {t('about.docs')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
className="h-auto p-0 text-[14px] text-blue-500 hover:text-blue-600 font-medium" 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')} {t('about.github')}
</Button> </Button>

View File

@@ -16,6 +16,7 @@ interface AgentsState {
createAgent: (name: string, options?: { inheritWorkspace?: boolean }) => Promise<void>; createAgent: (name: string, options?: { inheritWorkspace?: boolean }) => Promise<void>;
updateAgent: (agentId: string, name: string) => Promise<void>; updateAgent: (agentId: string, name: string) => Promise<void>;
updateAgentModel: (agentId: string, modelRef: string | null) => Promise<void>; updateAgentModel: (agentId: string, modelRef: string | null) => Promise<void>;
updateDefaultModel: (modelRef: string | null) => Promise<void>;
deleteAgent: (agentId: string) => Promise<void>; deleteAgent: (agentId: string) => Promise<void>;
assignChannel: (agentId: string, channelType: ChannelType) => Promise<void>; assignChannel: (agentId: string, channelType: ChannelType) => Promise<void>;
removeChannel: (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) => { deleteAgent: async (agentId: string) => {
set({ error: null }); set({ error: null });
try { try {