feat(deskclaw): rebrand + vibe presets + chat model picker
This commit is contained in:
47
DESKCLAW_FEATURES.md
Normal file
47
DESKCLAW_FEATURES.md
Normal 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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;',
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 件",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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": "モダンで美しいインターフェース",
|
||||
|
||||
@@ -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 个活动步骤",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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": "现代美观的界面",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user