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
|
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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;',
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 件",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -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": "モダンで美しいインターフェース",
|
||||||
|
|||||||
@@ -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 个活动步骤",
|
||||||
|
|||||||
@@ -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}}",
|
||||||
|
|||||||
@@ -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": "现代美观的界面",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user