add ccr statusline command

This commit is contained in:
musistudio
2025-08-15 23:50:57 +08:00
parent a265cbdce6
commit 0e509528c2
21 changed files with 2961 additions and 156 deletions

View File

@@ -40,4 +40,5 @@ This project is a TypeScript-based router for Claude Code requests. It allows ro
- **Providers and Transformers**: The application supports multiple LLM providers. Transformers adapt the request and response formats for different provider APIs. - **Providers and Transformers**: The application supports multiple LLM providers. Transformers adapt the request and response formats for different provider APIs.
- **Claude Code Integration**: When a user runs `ccr code`, the command is forwarded to the running router service. The service then processes the request, applies routing rules, and sends it to the configured LLM. If the service isn't running, `ccr code` will attempt to start it automatically. - **Claude Code Integration**: When a user runs `ccr code`, the command is forwarded to the running router service. The service then processes the request, applies routing rules, and sends it to the configured LLM. If the service isn't running, `ccr code` will attempt to start it automatically.
- **Dependencies**: The project is built with `esbuild`. It has a key local dependency `@musistudio/llms`, which probably contains the core logic for interacting with different LLM APIs. - **Dependencies**: The project is built with `esbuild`. It has a key local dependency `@musistudio/llms`, which probably contains the core logic for interacting with different LLM APIs.
- `@musistudio/llms` is implemented based on `fastify` and exposes `fastify`'s hook and middleware interfaces, allowing direct use of `server.addHook`. - `@musistudio/llms` is implemented based on `fastify` and exposes `fastify`'s hook and middleware interfaces, allowing direct use of `server.addHook`.
- 无论如何你都不能自动提交git

View File

@@ -329,7 +329,7 @@ You can also create your own transformers and load them via the `transformers` f
{ {
"transformers": [ "transformers": [
{ {
"path": "$HOME/.claude-code-router/plugins/gemini-cli.js", "path": "/User/xxx/.claude-code-router/plugins/gemini-cli.js",
"options": { "options": {
"project": "xxx" "project": "xxx"
} }
@@ -361,7 +361,7 @@ In your `config.json`:
```json ```json
{ {
"CUSTOM_ROUTER_PATH": "$HOME/.claude-code-router/custom-router.js" "CUSTOM_ROUTER_PATH": "/User/xxx/.claude-code-router/custom-router.js"
} }
``` ```
@@ -370,7 +370,7 @@ The custom router file must be a JavaScript module that exports an `async` funct
Here is an example of a `custom-router.js` based on `custom-router.example.js`: Here is an example of a `custom-router.js` based on `custom-router.example.js`:
```javascript ```javascript
// $HOME/.claude-code-router/custom-router.js // /User/xxx/.claude-code-router/custom-router.js
/** /**
* A custom router function to determine which model to use based on the request. * A custom router function to determine which model to use based on the request.

View File

@@ -301,7 +301,7 @@ Transformers 允许您修改请求和响应负载,以确保与不同提供商
{ {
"transformers": [ "transformers": [
{ {
"path": "$HOME/.claude-code-router/plugins/gemini-cli.js", "path": "/User/xxx/.claude-code-router/plugins/gemini-cli.js",
"options": { "options": {
"project": "xxx" "project": "xxx"
} }
@@ -333,7 +333,7 @@ Transformers 允许您修改请求和响应负载,以确保与不同提供商
```json ```json
{ {
"CUSTOM_ROUTER_PATH": "$HOME/.claude-code-router/custom-router.js" "CUSTOM_ROUTER_PATH": "/User/xxx/.claude-code-router/custom-router.js"
} }
``` ```
@@ -342,7 +342,7 @@ Transformers 允许您修改请求和响应负载,以确保与不同提供商
这是一个基于 `custom-router.example.js` 的 `custom-router.js` 示例: 这是一个基于 `custom-router.example.js` 的 `custom-router.js` 示例:
```javascript ```javascript
// $HOME/.claude-code-router/custom-router.js // /User/xxx/.claude-code-router/custom-router.js
/** /**
* 一个自定义路由函数,用于根据请求确定使用哪个模型。 * 一个自定义路由函数,用于根据请求确定使用哪个模型。

View File

@@ -1,121 +0,0 @@
{
"Providers": [
{
"name": "openrouter",
"api_base_url": "https://openrouter.ai/api/v1/chat/completions",
"api_key": "sk-xxx",
"models": [
"google/gemini-2.5-pro-preview",
"anthropic/claude-sonnet-4",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3.7-sonnet:thinking"
],
"transformer": {
"use": ["openrouter"]
}
},
{
"name": "deepseek",
"api_base_url": "https://api.deepseek.com/chat/completions",
"api_key": "sk-xxx",
"models": ["deepseek-chat", "deepseek-reasoner"],
"transformer": {
"use": ["deepseek"],
"deepseek-chat": {
"use": ["tooluse"]
}
}
},
{
"name": "ollama",
"api_base_url": "http://localhost:11434/v1/chat/completions",
"api_key": "ollama",
"models": ["qwen2.5-coder:latest"]
},
{
"name": "gemini",
"api_base_url": "https://generativelanguage.googleapis.com/v1beta/models/",
"api_key": "sk-xxx",
"models": ["gemini-2.5-flash", "gemini-2.5-pro"],
"transformer": {
"use": ["gemini"]
}
},
{
"name": "volcengine",
"api_base_url": "https://ark.cn-beijing.volces.com/api/v3/chat/completions",
"api_key": "sk-xxx",
"models": ["deepseek-v3-250324", "deepseek-r1-250528"],
"transformer": {
"use": ["deepseek"]
}
},
{
"name": "siliconflow",
"api_base_url": "https://api.siliconflow.cn/v1/chat/completions",
"api_key": "sk-xxx",
"models": ["moonshotai/Kimi-K2-Instruct"],
"transformer": {
"use": [
[
"maxtoken",
{
"max_tokens": 16384
}
]
]
}
},
{
"name": "modelscope",
"api_base_url": "https://api-inference.modelscope.cn/v1/chat/completions",
"api_key": "",
"models": ["Qwen/Qwen3-Coder-480B-A35B-Instruct", "Qwen/Qwen3-235B-A22B-Thinking-2507"],
"transformer": {
"use": [
[
"maxtoken",
{
"max_tokens": 65536
}
],
"enhancetool"
],
"Qwen/Qwen3-235B-A22B-Thinking-2507": {
"use": ["reasoning"]
}
}
},
{
"name": "dashscope",
"api_base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
"api_key": "",
"models": ["qwen3-coder-plus"],
"transformer": {
"use": [
[
"maxtoken",
{
"max_tokens": 65536
}
],
"enhancetool"
]
}
}
],
"Router": {
"default": "deepseek,deepseek-chat",
"background": "ollama,qwen2.5-coder:latest",
"think": "deepseek,deepseek-reasoner",
"longContext": "openrouter,google/gemini-2.5-pro-preview",
"longContextThreshold": 60000,
"webSearch": "gemini,gemini-2.5-flash"
},
"APIKEY": "your-secret-key",
"HOST": "0.0.0.0",
"API_TIMEOUT_MS": 600000,
"NON_INTERACTIVE_MODE": false,
"LOG": true,
"LOG_LEVEL": "info"
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@musistudio/claude-code-router", "name": "@musistudio/claude-code-router",
"version": "1.0.37", "version": "1.0.38",
"description": "Use Claude Code without an Anthropics account and route it to another LLM provider", "description": "Use Claude Code without an Anthropics account and route it to another LLM provider",
"bin": { "bin": {
"ccr": "./dist/cli.js" "ccr": "./dist/cli.js"
@@ -20,7 +20,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fastify/static": "^8.2.0", "@fastify/static": "^8.2.0",
"@musistudio/llms": "^1.0.23", "@musistudio/llms": "^1.0.24",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"json5": "^2.2.3", "json5": "^2.2.3",
"openurl": "^1.1.1", "openurl": "^1.1.1",

10
pnpm-lock.yaml generated
View File

@@ -12,8 +12,8 @@ importers:
specifier: ^8.2.0 specifier: ^8.2.0
version: 8.2.0 version: 8.2.0
'@musistudio/llms': '@musistudio/llms':
specifier: ^1.0.23 specifier: ^1.0.24
version: 1.0.23(ws@8.18.3)(zod@3.25.67) version: 1.0.24(ws@8.18.3)(zod@3.25.67)
dotenv: dotenv:
specifier: ^16.4.7 specifier: ^16.4.7
version: 16.6.1 version: 16.6.1
@@ -260,8 +260,8 @@ packages:
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
engines: {node: '>=8'} engines: {node: '>=8'}
'@musistudio/llms@1.0.23': '@musistudio/llms@1.0.24':
resolution: {integrity: sha512-+ygbTi6vsNXj9OTD/w/1ai6rYGB/EOHWO+GmpMKCA66HrE8czAQ9UbZz4SjSLqLFGxokBs+ru7ntM4w8TVq6/Q==} resolution: {integrity: sha512-Hz6ZT92/ZM/eR5kTdCBHD6zoEMOvT5u6g/vfCir5Hwvl4QGHk3g30EmX1pZAXJf83kLnB/lSEq/HQimFIXHIhQ==}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -1112,7 +1112,7 @@ snapshots:
'@lukeed/ms@2.0.2': {} '@lukeed/ms@2.0.2': {}
'@musistudio/llms@1.0.23(ws@8.18.3)(zod@3.25.67)': '@musistudio/llms@1.0.24(ws@8.18.3)(zod@3.25.67)':
dependencies: dependencies:
'@anthropic-ai/sdk': 0.54.0 '@anthropic-ai/sdk': 0.54.0
'@fastify/cors': 11.0.1 '@fastify/cors': 11.0.1

View File

@@ -2,6 +2,7 @@
import { run } from "./index"; import { run } from "./index";
import { showStatus } from "./utils/status"; import { showStatus } from "./utils/status";
import { executeCodeCommand } from "./utils/codeCommand"; import { executeCodeCommand } from "./utils/codeCommand";
import { parseStatusLineData, type StatusLineInput } from "./utils/statusline";
import { import {
cleanupPidFile, cleanupPidFile,
isServiceRunning, isServiceRunning,
@@ -23,6 +24,7 @@ Commands:
stop Stop server stop Stop server
restart Restart server restart Restart server
status Show server status status Show server status
statusline Show status line information
code Execute claude command code Execute claude command
ui Open the web UI in browser ui Open the web UI in browser
-v, version Show version information -v, version Show version information
@@ -83,6 +85,28 @@ async function main() {
case "status": case "status":
await showStatus(); await showStatus();
break; break;
case "statusline":
// 从stdin读取JSON输入
let inputData = "";
process.stdin.setEncoding("utf-8");
process.stdin.on("readable", () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
inputData += chunk;
}
});
process.stdin.on("end", async () => {
try {
const input: StatusLineInput = JSON.parse(inputData);
const statusLine = await parseStatusLineData(input);
console.log(statusLine);
} catch (error) {
console.error("Error parsing status line data:", error);
process.exit(1);
}
});
break;
case "code": case "code":
if (!isServiceRunning()) { if (!isServiceRunning()) {
console.log("Service not running, starting service..."); console.log("Service not running, starting service...");

747
src/utils/statusline.ts Normal file
View File

@@ -0,0 +1,747 @@
import fs from "node:fs/promises";
import { execSync } from "child_process";
import path from "node:path";
import { CONFIG_FILE, HOME_DIR } from "../constants";
import JSON5 from "json5";
export interface StatusLineModuleConfig {
type: string;
icon?: string;
text: string;
color?: string;
background?: string;
}
export interface StatusLineThemeConfig {
modules: StatusLineModuleConfig[];
}
export interface StatusLineInput {
hook_event_name: string;
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
}
export interface AssistantMessage {
type: "assistant";
message: {
model: string;
usage: {
input_tokens: number;
output_tokens: number;
};
};
}
// ANSIColor代码
const COLORS: Record<string, string> = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
// 标准颜色
black: "\x1b[30m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
// 亮色
bright_black: "\x1b[90m",
bright_red: "\x1b[91m",
bright_green: "\x1b[92m",
bright_yellow: "\x1b[93m",
bright_blue: "\x1b[94m",
bright_magenta: "\x1b[95m",
bright_cyan: "\x1b[96m",
bright_white: "\x1b[97m",
// 背景颜色
bg_black: "\x1b[40m",
bg_red: "\x1b[41m",
bg_green: "\x1b[42m",
bg_yellow: "\x1b[43m",
bg_blue: "\x1b[44m",
bg_magenta: "\x1b[45m",
bg_cyan: "\x1b[46m",
bg_white: "\x1b[47m",
// 亮背景色
bg_bright_black: "\x1b[100m",
bg_bright_red: "\x1b[101m",
bg_bright_green: "\x1b[102m",
bg_bright_yellow: "\x1b[103m",
bg_bright_blue: "\x1b[104m",
bg_bright_magenta: "\x1b[105m",
bg_bright_cyan: "\x1b[106m",
bg_bright_white: "\x1b[107m",
};
// 使用TrueColor(24位色)支持十六进制颜色
const TRUE_COLOR_PREFIX = "\x1b[38;2;";
const TRUE_COLOR_BG_PREFIX = "\x1b[48;2;";
// 将十六进制颜色转为RGB格式
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
// 移除#和空格
hex = hex.replace(/^#/, '').trim();
// 处理简写形式 (#RGB -> #RRGGBB)
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6) {
return null;
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// 验证RGB值是否有效
if (isNaN(r) || isNaN(g) || isNaN(b) || r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
return null;
}
return { r, g, b };
}
// 获取颜色代码
function getColorCode(colorName: string): string {
// 检查是否是十六进制颜色
if (colorName.startsWith('#') || /^[0-9a-fA-F]{6}$/.test(colorName) || /^[0-9a-fA-F]{3}$/.test(colorName)) {
const rgb = hexToRgb(colorName);
if (rgb) {
return `${TRUE_COLOR_PREFIX}${rgb.r};${rgb.g};${rgb.b}m`;
}
}
// 默认返回空字符串
return "";
}
// 变量替换函数,支持{{var}}格式的变量替换
function replaceVariables(text: string, variables: Record<string, string>): string {
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] || match;
});
}
// 默认主题配置 - 使用Nerd Fonts图标和美观配色
const DEFAULT_THEME: StatusLineThemeConfig = {
modules: [
{
type: "workDir",
icon: "󰉋", // nf-md-folder_outline
text: "{{workDirName}}",
color: "bright_blue"
},
{
type: "gitBranch",
icon: "", // nf-dev-git_branch
text: "{{gitBranch}}",
color: "bright_magenta"
},
{
type: "model",
icon: "󰚩", // nf-md-robot_outline
text: "{{model}}",
color: "bright_cyan"
},
{
type: "usage",
icon: "↑", // 上箭头
text: "{{inputTokens}}",
color: "bright_green"
},
{
type: "usage",
icon: "↓", // 下箭头
text: "{{outputTokens}}",
color: "bright_yellow"
}
]
};
// Powerline风格主题配置
const POWERLINE_THEME: StatusLineThemeConfig = {
modules: [
{
type: "workDir",
icon: "󰉋", // nf-md-folder_outline
text: "{{workDirName}}",
color: "white",
background: "bg_bright_blue"
},
{
type: "gitBranch",
icon: "", // nf-dev-git_branch
text: "{{gitBranch}}",
color: "white",
background: "bg_bright_magenta"
},
{
type: "model",
icon: "󰚩", // nf-md-robot_outline
text: "{{model}}",
color: "white",
background: "bg_bright_cyan"
},
{
type: "usage",
icon: "↑", // 上箭头
text: "{{inputTokens}}",
color: "white",
background: "bg_bright_green"
},
{
type: "usage",
icon: "↓", // 下箭头
text: "{{outputTokens}}",
color: "white",
background: "bg_bright_yellow"
}
]
};
// 简单文本主题配置 - 用于图标无法显示时的fallback
const SIMPLE_THEME: StatusLineThemeConfig = {
modules: [
{
type: "workDir",
icon: "",
text: "{{workDirName}}",
color: "bright_blue"
},
{
type: "gitBranch",
icon: "",
text: "{{gitBranch}}",
color: "bright_magenta"
},
{
type: "model",
icon: "",
text: "{{model}}",
color: "bright_cyan"
},
{
type: "usage",
icon: "↑",
text: "{{inputTokens}}",
color: "bright_green"
},
{
type: "usage",
icon: "↓",
text: "{{outputTokens}}",
color: "bright_yellow"
}
]
};
// 格式化usage信息如果大于1000则使用k单位
function formatUsage(input_tokens: number, output_tokens: number): string {
if (input_tokens > 1000 || output_tokens > 1000) {
const inputFormatted = input_tokens > 1000 ? `${(input_tokens / 1000).toFixed(1)}k` : `${input_tokens}`;
const outputFormatted = output_tokens > 1000 ? `${(output_tokens / 1000).toFixed(1)}k` : `${output_tokens}`;
return `${inputFormatted} ${outputFormatted}`;
}
return `${input_tokens} ${output_tokens}`;
}
// 读取用户主目录的主题配置
async function getProjectThemeConfig(): Promise<{ theme: StatusLineThemeConfig | null, style: string }> {
try {
// 只使用主目录的固定配置文件
const configPath = CONFIG_FILE;
// 检查配置文件是否存在
try {
await fs.access(configPath);
} catch {
return { theme: null, style: 'default' };
}
const configContent = await fs.readFile(configPath, "utf-8");
const config = JSON5.parse(configContent);
// 检查是否有StatusLine配置
if (config.StatusLine) {
// 获取当前使用的风格默认为default
const currentStyle = config.StatusLine.currentStyle || 'default';
// 检查是否有对应风格的配置
if (config.StatusLine[currentStyle] && config.StatusLine[currentStyle].modules) {
return { theme: config.StatusLine[currentStyle], style: currentStyle };
}
}
} catch (error) {
// 如果读取失败返回null
// console.error("Failed to read theme config:", error);
}
return { theme: null, style: 'default' };
}
// 检查是否应该使用简单主题fallback方案
// 当环境变量 USE_SIMPLE_ICONS 被设置时或者当检测到可能不支持Nerd Fonts的终端时
function shouldUseSimpleTheme(): boolean {
// 检查环境变量
if (process.env.USE_SIMPLE_ICONS === 'true') {
return true;
}
// 检查终端类型(一些常见的不支持复杂图标的终端)
const term = process.env.TERM || '';
const unsupportedTerms = ['dumb', 'unknown'];
if (unsupportedTerms.includes(term)) {
return true;
}
// 默认情况下假设终端支持Nerd Fonts
return false;
}
// 检查Nerd Fonts图标是否能正确显示
// 通过检查终端字体信息或使用试探性方法
function canDisplayNerdFonts(): boolean {
// 如果环境变量明确指定使用简单图标则不能显示Nerd Fonts
if (process.env.USE_SIMPLE_ICONS === 'true') {
return false;
}
// 检查一些常见的支持Nerd Fonts的终端环境变量
const fontEnvVars = ['NERD_FONT', 'NERDFONT', 'FONT'];
for (const envVar of fontEnvVars) {
const value = process.env[envVar];
if (value && (value.includes('Nerd') || value.includes('nerd'))) {
return true;
}
}
// 检查终端类型
const termProgram = process.env.TERM_PROGRAM || '';
const supportedTerminals = ['iTerm.app', 'vscode', 'Hyper', 'kitty', 'alacritty'];
if (supportedTerminals.includes(termProgram)) {
return true;
}
// 检查COLORTERM环境变量
const colorTerm = process.env.COLORTERM || '';
if (colorTerm.includes('truecolor') || colorTerm.includes('24bit')) {
return true;
}
// 默认情况下假设可以显示Nerd Fonts但允许用户通过环境变量覆盖
return process.env.USE_SIMPLE_ICONS !== 'true';
}
// 检查特定Unicode字符是否能正确显示
// 这是一个简单的试探性检查
function canDisplayUnicodeCharacter(char: string): boolean {
// 对于Nerd Fonts图标我们假设支持UTF-8的终端可以显示
// 但实际上很难准确检测,所以我们依赖环境变量和终端类型检测
try {
// 检查终端是否支持UTF-8
const lang = process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '';
if (lang.includes('UTF-8') || lang.includes('utf8') || lang.includes('UTF8')) {
return true;
}
// 检查LC_*环境变量
const lcVars = ['LC_ALL', 'LC_CTYPE', 'LANG'];
for (const lcVar of lcVars) {
const value = process.env[lcVar];
if (value && (value.includes('UTF-8') || value.includes('utf8'))) {
return true;
}
}
} catch (e) {
// 如果检查失败默认返回true
return true;
}
// 默认情况下,假设可以显示
return true;
}
export async function parseStatusLineData(input: StatusLineInput): Promise<string> {
try {
// 检查是否应该使用简单主题
const useSimpleTheme = shouldUseSimpleTheme();
// 检查是否可以显示Nerd Fonts图标
const canDisplayNerd = canDisplayNerdFonts();
// 确定使用的主题如果用户强制使用简单主题或无法显示Nerd Fonts则使用简单主题
const effectiveTheme = useSimpleTheme || !canDisplayNerd ? SIMPLE_THEME : DEFAULT_THEME;
// 获取主目录的主题配置,如果没有则使用确定的默认配置
const { theme: projectTheme, style: currentStyle } = await getProjectThemeConfig();
const theme = projectTheme || effectiveTheme;
// 获取当前工作目录和Git分支
const workDir = input.workspace.current_dir;
let gitBranch = "";
try {
// 尝试获取Git分支名
gitBranch = execSync("git branch --show-current", {
cwd: workDir,
stdio: ["pipe", "pipe", "ignore"],
})
.toString()
.trim();
} catch (error) {
// 如果不是Git仓库或获取失败则忽略错误
}
// 从transcript_path文件中读取最后一条assistant消息
const transcriptContent = await fs.readFile(input.transcript_path, "utf-8");
const lines = transcriptContent.trim().split("\n");
// 反向遍历寻找最后一条assistant消息
let model = "";
let inputTokens = 0;
let outputTokens = 0;
for (let i = lines.length - 1; i >= 0; i--) {
try {
const message: AssistantMessage = JSON.parse(lines[i]);
if (message.type === "assistant" && message.message.model) {
model = message.message.model;
if (message.message.usage) {
inputTokens = message.message.usage.input_tokens;
outputTokens = message.message.usage.output_tokens;
}
break;
}
} catch (parseError) {
// 忽略解析错误,继续查找
continue;
}
}
// 如果没有从transcript中获取到模型名称则尝试从配置文件中获取
if (!model) {
try {
// 获取项目配置文件路径
const projectConfigPath = path.join(workDir, ".claude-code-router", "config.json");
let configPath = projectConfigPath;
// 检查项目配置文件是否存在,如果不存在则使用用户主目录的配置文件
try {
await fs.access(projectConfigPath);
} catch {
configPath = CONFIG_FILE;
}
// 读取配置文件
const configContent = await fs.readFile(configPath, "utf-8");
const config = JSON5.parse(configContent);
// 从Router字段的default内容中获取模型名称
if (config.Router && config.Router.default) {
const [, defaultModel] = config.Router.default.split(",");
if (defaultModel) {
model = defaultModel.trim();
}
}
} catch (configError) {
// 如果配置文件读取失败,则忽略错误
}
}
// 如果仍然没有获取到模型名称则使用传入的JSON数据中的model字段的display_name
if (!model) {
model = input.model.display_name;
}
// 获取工作目录名
const workDirName = workDir.split("/").pop() || "";
// 格式化usage信息
const usage = formatUsage(inputTokens, outputTokens);
const [formattedInputTokens, formattedOutputTokens] = usage.split(" ");
// 定义变量替换映射
const variables = {
workDirName,
gitBranch,
model,
inputTokens: formattedInputTokens,
outputTokens: formattedOutputTokens
};
// 确定使用的风格
const isPowerline = currentStyle === 'powerline';
// 根据风格渲染状态行
if (isPowerline) {
return renderPowerlineStyle(theme, variables);
} else {
return renderDefaultStyle(theme, variables);
}
} catch (error) {
// 发生错误时返回空字符串
return "";
}
}
// 读取用户主目录的主题配置(指定风格)
async function getProjectThemeConfigForStyle(style: string): Promise<StatusLineThemeConfig | null> {
try {
// 只使用主目录的固定配置文件
const configPath = CONFIG_FILE;
// 检查配置文件是否存在
try {
await fs.access(configPath);
} catch {
return null;
}
const configContent = await fs.readFile(configPath, "utf-8");
const config = JSON5.parse(configContent);
// 检查是否有StatusLine配置
if (config.StatusLine && config.StatusLine[style] && config.StatusLine[style].modules) {
return config.StatusLine[style];
}
} catch (error) {
// 如果读取失败返回null
// console.error("Failed to read theme config:", error);
}
return null;
}
// 渲染默认风格的状态行
function renderDefaultStyle(
theme: StatusLineThemeConfig,
variables: Record<string, string>
): string {
const modules = theme.modules || DEFAULT_THEME.modules;
const parts: string[] = [];
// 遍历模块数组,渲染每个模块
for (let i = 0; i < Math.min(modules.length, 5); i++) {
const module = modules[i];
const color = module.color ? getColorCode(module.color) : "";
const background = module.background ? getColorCode(module.background) : "";
const icon = module.icon || "";
const text = replaceVariables(module.text, variables);
// 如果text为空且不是usage类型则跳过该模块
if (!text && module.type !== "usage") {
continue;
}
// 构建模块字符串
let part = `${background}${color}`;
if (icon) {
part += `${icon} `;
}
part += `${text}${COLORS.reset}`;
parts.push(part);
}
// 使用空格连接所有部分
return parts.join(" ");
}
// Powerline符号
const SEP_RIGHT = "\uE0B0"; // 
// 颜色编号256色表
const COLOR_MAP: Record<string, number> = {
// 基础颜色映射到256色
black: 0,
red: 1,
green: 2,
yellow: 3,
blue: 4,
magenta: 5,
cyan: 6,
white: 7,
bright_black: 8,
bright_red: 9,
bright_green: 10,
bright_yellow: 11,
bright_blue: 12,
bright_magenta: 13,
bright_cyan: 14,
bright_white: 15,
// 亮背景色映射
bg_black: 0,
bg_red: 1,
bg_green: 2,
bg_yellow: 3,
bg_blue: 4,
bg_magenta: 5,
bg_cyan: 6,
bg_white: 7,
bg_bright_black: 8,
bg_bright_red: 9,
bg_bright_green: 10,
bg_bright_yellow: 11,
bg_bright_blue: 12,
bg_bright_magenta: 13,
bg_bright_cyan: 14,
bg_bright_white: 15,
// 自定义颜色映射
bg_bright_orange: 202,
bg_bright_purple: 129,
};
// 获取TrueColor的RGB值
function getTrueColorRgb(colorName: string): { r: number; g: number; b: number } | null {
// 如果是预定义颜色返回对应RGB
if (COLOR_MAP[colorName] !== undefined) {
const color256 = COLOR_MAP[colorName];
return color256ToRgb(color256);
}
// 处理十六进制颜色
if (colorName.startsWith('#') || /^[0-9a-fA-F]{6}$/.test(colorName) || /^[0-9a-fA-F]{3}$/.test(colorName)) {
return hexToRgb(colorName);
}
// 处理背景色十六进制
if (colorName.startsWith('bg_#')) {
return hexToRgb(colorName.substring(3));
}
return null;
}
// 将256色表索引转换为RGB值
function color256ToRgb(index: number): { r: number; g: number; b: number } | null {
if (index < 0 || index > 255) return null;
// ANSI 256色表转换
if (index < 16) {
// 基本颜色
const basicColors = [
[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
[0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0],
[0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]
];
return { r: basicColors[index][0], g: basicColors[index][1], b: basicColors[index][2] };
} else if (index < 232) {
// 216色6×6×6的颜色立方体
const i = index - 16;
const r = Math.floor(i / 36);
const g = Math.floor((i % 36) / 6);
const b = i % 6;
const rgb = [0, 95, 135, 175, 215, 255];
return { r: rgb[r], g: rgb[g], b: rgb[b] };
} else {
// 灰度色
const gray = 8 + (index - 232) * 10;
return { r: gray, g: gray, b: gray };
}
}
// 生成一个无缝拼接的段:文本在 bgN 上显示,分隔符从 bgN 过渡到 nextBgN
function segment(text: string, textFg: string, bgColor: string, nextBgColor: string | null): string {
const bgRgb = getTrueColorRgb(bgColor);
if (!bgRgb) {
// 如果无法获取RGB使用默认蓝色背景
const defaultBlueRgb = { r: 33, g: 150, b: 243 };
const curBg = `\x1b[48;2;${defaultBlueRgb.r};${defaultBlueRgb.g};${defaultBlueRgb.b}m`;
const fgColor = `\x1b[38;2;255;255;255m`;
const body = `${curBg}${fgColor} ${text} \x1b[0m`;
return body;
}
const curBg = `\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
// 获取前景色RGB
let fgRgb = { r: 255, g: 255, b: 255 }; // 默认前景色为白色
const textFgRgb = getTrueColorRgb(textFg);
if (textFgRgb) {
fgRgb = textFgRgb;
}
const fgColor = `\x1b[38;2;${fgRgb.r};${fgRgb.g};${fgRgb.b}m`;
const body = `${curBg}${fgColor} ${text} \x1b[0m`;
if (nextBgColor != null) {
const nextBgRgb = getTrueColorRgb(nextBgColor);
if (nextBgRgb) {
// 分隔符:前景色是当前段的背景色,背景色是下一段的背景色
const sepCurFg = `\x1b[38;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
const sepNextBg = `\x1b[48;2;${nextBgRgb.r};${nextBgRgb.g};${nextBgRgb.b}m`;
const sep = `${sepCurFg}${sepNextBg}${SEP_RIGHT}\x1b[0m`;
return body + sep;
}
// 如果没有下一个背景色,假设终端背景为黑色并渲染黑色箭头
const sepCurFg = `\x1b[38;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
const sepNextBg = `\x1b[48;2;0;0;0m`; // 黑色背景
const sep = `${sepCurFg}${sepNextBg}${SEP_RIGHT}\x1b[0m`;
return body + sep;
}
return body;
}
// 渲染Powerline风格的状态行
function renderPowerlineStyle(
theme: StatusLineThemeConfig,
variables: Record<string, string>
): string {
const modules = theme.modules || POWERLINE_THEME.modules;
const segments: string[] = [];
// 遍历模块数组,渲染每个模块
for (let i = 0; i < Math.min(modules.length, 5); i++) {
const module = modules[i];
const color = module.color || "white";
const backgroundName = module.background || "";
const icon = module.icon || "";
const text = replaceVariables(module.text, variables);
// 如果text为空且不是usage类型则跳过该模块
if (!text && module.type !== "usage") {
continue;
}
// 构建显示文本
let displayText = "";
if (icon) {
displayText += `${icon} `;
}
displayText += text;
// 获取下一个模块的背景色(用于分隔符)
let nextBackground: string | null = null;
if (i < modules.length - 1) {
const nextModule = modules[i + 1];
nextBackground = nextModule.background || null;
}
// 使用模块定义的背景色或者为Powerline风格提供默认背景色
const actualBackground = backgroundName || "bg_bright_blue";
// 生成段,支持十六进制颜色
const segmentStr = segment(displayText, color, actualBackground, nextBackground);
segments.push(segmentStr);
}
return segments.join("");
}

279
ui/package-lock.json generated
View File

@@ -8,11 +8,13 @@
"name": "temp-project", "name": "temp-project",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -21,6 +23,8 @@
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.6.1", "react-i18next": "^15.6.1",
"react-router-dom": "^7.7.0", "react-router-dom": "^7.7.0",
@@ -1086,6 +1090,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@monaco-editor/loader": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
"integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
"license": "MIT",
"dependencies": {
"@monaco-editor/loader": "^1.5.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1514,6 +1541,129 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@@ -1650,12 +1800,53 @@
} }
} }
}, },
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": { "node_modules/@radix-ui/rect": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==",
"license": "MIT"
},
"node_modules/@react-dnd/invariant": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==",
"license": "MIT"
},
"node_modules/@react-dnd/shallowequal": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27", "version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -2955,6 +3146,17 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dnd-core": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
"license": "MIT",
"dependencies": {
"@react-dnd/asap": "^5.0.1",
"@react-dnd/invariant": "^4.0.1",
"redux": "^4.2.0"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.192", "version": "1.5.192",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz",
@@ -3221,7 +3423,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
@@ -3438,6 +3639,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/html-parse-stringify": { "node_modules/html-parse-stringify": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -4016,6 +4226,13 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/monaco-editor": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
"license": "MIT",
"peer": true
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4252,6 +4469,45 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
"license": "MIT",
"dependencies": {
"@react-dnd/invariant": "^4.0.1",
"@react-dnd/shallowequal": "^4.0.1",
"dnd-core": "^16.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"@types/hoist-non-react-statics": ">= 3.3.1",
"@types/node": ">= 12",
"@types/react": ">= 16",
"react": ">= 16.14"
},
"peerDependenciesMeta": {
"@types/hoist-non-react-statics": {
"optional": true
},
"@types/node": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dnd-html5-backend": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
"license": "MIT",
"dependencies": {
"dnd-core": "^16.0.1"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.1", "version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
@@ -4290,6 +4546,12 @@
} }
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -4407,6 +4669,15 @@
} }
} }
}, },
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4545,6 +4816,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",

View File

@@ -10,13 +10,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-tooltip": "^1.2.7",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -25,6 +25,9 @@
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.6.1", "react-i18next": "^15.6.1",
"react-router-dom": "^7.7.0", "react-router-dom": "^7.7.0",

240
ui/pnpm-lock.yaml generated
View File

@@ -26,6 +26,9 @@ importers:
'@radix-ui/react-switch': '@radix-ui/react-switch':
specifier: ^1.2.5 specifier: ^1.2.5
version: 1.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-tooltip':
specifier: ^1.2.7
version: 1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.11 specifier: ^4.1.11
version: 4.1.11(vite@7.0.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)) version: 4.1.11(vite@7.0.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))
@@ -50,6 +53,15 @@ importers:
react: react:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0 version: 19.1.0
react-colorful:
specifier: ^5.6.1
version: 5.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-dnd:
specifier: ^16.0.1
version: 16.0.1(@types/node@24.1.0)(@types/react@19.1.8)(react@19.1.0)
react-dnd-html5-backend:
specifier: ^16.0.1
version: 16.0.1
react-dom: react-dom:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
@@ -489,6 +501,9 @@ packages:
'@radix-ui/primitive@1.1.2': '@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-arrow@1.1.7': '@radix-ui/react-arrow@1.1.7':
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
peerDependencies: peerDependencies:
@@ -546,6 +561,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-dismissable-layer@1.1.11':
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.1.2': '@radix-ui/react-focus-guards@1.1.2':
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
peerDependencies: peerDependencies:
@@ -616,6 +644,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-popper@1.2.8':
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-portal@1.1.9': '@radix-ui/react-portal@1.1.9':
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
peerDependencies: peerDependencies:
@@ -642,6 +683,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3': '@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies: peerDependencies:
@@ -677,6 +731,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-tooltip@1.2.8':
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1': '@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies: peerDependencies:
@@ -749,9 +816,31 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-visually-hidden@1.2.3':
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/rect@1.1.1': '@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
'@react-dnd/asap@5.0.2':
resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==}
'@react-dnd/invariant@4.0.2':
resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==}
'@react-dnd/shallowequal@4.0.2':
resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
'@rolldown/pluginutils@1.0.0-beta.27': '@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@@ -1162,6 +1251,9 @@ packages:
detect-node-es@1.1.0: detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dnd-core@16.0.1:
resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
electron-to-chromium@1.5.190: electron-to-chromium@1.5.190:
resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==}
@@ -1320,6 +1412,9 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
html-parse-stringify@3.0.1: html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
@@ -1586,6 +1681,30 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-colorful@5.6.1:
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
react-dnd-html5-backend@16.0.1:
resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
react-dnd@16.0.1:
resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==}
peerDependencies:
'@types/hoist-non-react-statics': '>= 3.3.1'
'@types/node': '>= 12'
'@types/react': '>= 16'
react: '>= 16.14'
peerDependenciesMeta:
'@types/hoist-non-react-statics':
optional: true
'@types/node':
optional: true
'@types/react':
optional: true
react-dom@19.1.0: react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies: peerDependencies:
@@ -1607,6 +1726,9 @@ packages:
typescript: typescript:
optional: true optional: true
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-refresh@0.17.0: react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1662,6 +1784,9 @@ packages:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2197,6 +2322,8 @@ snapshots:
'@radix-ui/primitive@1.1.2': {} '@radix-ui/primitive@1.1.2': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2253,6 +2380,19 @@ snapshots:
'@types/react': 19.1.8 '@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8) '@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.8)(react@19.1.0)': '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.8)(react@19.1.0)':
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
@@ -2327,6 +2467,24 @@ snapshots:
'@types/react': 19.1.8 '@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8) '@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/rect': 1.1.1
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2347,6 +2505,16 @@ snapshots:
'@types/react': 19.1.8 '@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8) '@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
@@ -2378,6 +2546,26 @@ snapshots:
'@types/react': 19.1.8 '@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8) '@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)': '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
@@ -2432,8 +2620,23 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.1.8 '@types/react': 19.1.8
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/rect@1.1.1': {} '@radix-ui/rect@1.1.1': {}
'@react-dnd/asap@5.0.2': {}
'@react-dnd/invariant@4.0.2': {}
'@react-dnd/shallowequal@4.0.2': {}
'@rolldown/pluginutils@1.0.0-beta.27': {} '@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.45.1': '@rollup/rollup-android-arm-eabi@4.45.1':
@@ -2831,6 +3034,12 @@ snapshots:
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
dnd-core@16.0.1:
dependencies:
'@react-dnd/asap': 5.0.2
'@react-dnd/invariant': 4.0.2
redux: 4.2.1
electron-to-chromium@1.5.190: {} electron-to-chromium@1.5.190: {}
enhanced-resolve@5.18.2: enhanced-resolve@5.18.2:
@@ -3017,6 +3226,10 @@ snapshots:
has-flag@4.0.0: {} has-flag@4.0.0: {}
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
html-parse-stringify@3.0.1: html-parse-stringify@3.0.1:
dependencies: dependencies:
void-elements: 3.1.0 void-elements: 3.1.0
@@ -3222,6 +3435,27 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
react-colorful@5.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-dnd-html5-backend@16.0.1:
dependencies:
dnd-core: 16.0.1
react-dnd@16.0.1(@types/node@24.1.0)(@types/react@19.1.8)(react@19.1.0):
dependencies:
'@react-dnd/invariant': 4.0.2
'@react-dnd/shallowequal': 4.0.2
dnd-core: 16.0.1
fast-deep-equal: 3.1.3
hoist-non-react-statics: 3.3.2
react: 19.1.0
optionalDependencies:
'@types/node': 24.1.0
'@types/react': 19.1.8
react-dom@19.1.0(react@19.1.0): react-dom@19.1.0(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
@@ -3237,6 +3471,8 @@ snapshots:
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
typescript: 5.8.3 typescript: 5.8.3
react-is@16.13.1: {}
react-refresh@0.17.0: {} react-refresh@0.17.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0): react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0):
@@ -3282,6 +3518,10 @@ snapshots:
react@19.1.0: {} react@19.1.0: {}
redux@4.2.1:
dependencies:
'@babel/runtime': 7.27.6
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
reusify@1.1.0: {} reusify@1.1.0: {}

View File

@@ -1,7 +1,7 @@
import { createContext, useContext, useState, useEffect } from 'react'; import { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode, Dispatch, SetStateAction } from 'react'; import type { ReactNode, Dispatch, SetStateAction } from 'react';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
import type { Config } from '@/types'; import type { Config, StatusLineConfig } from '@/types';
interface ConfigContextType { interface ConfigContextType {
config: Config | null; config: Config | null;
@@ -78,6 +78,17 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
PROXY_URL: typeof data.PROXY_URL === 'string' ? data.PROXY_URL : '', PROXY_URL: typeof data.PROXY_URL === 'string' ? data.PROXY_URL : '',
transformers: Array.isArray(data.transformers) ? data.transformers : [], transformers: Array.isArray(data.transformers) ? data.transformers : [],
Providers: Array.isArray(data.Providers) ? data.Providers : [], Providers: Array.isArray(data.Providers) ? data.Providers : [],
StatusLine: data.StatusLine && typeof data.StatusLine === 'object' ? {
enabled: typeof data.StatusLine.enabled === 'boolean' ? data.StatusLine.enabled : false,
currentStyle: typeof data.StatusLine.currentStyle === 'string' ? data.StatusLine.currentStyle : 'default',
default: data.StatusLine.default && typeof data.StatusLine.default === 'object' && Array.isArray(data.StatusLine.default.modules) ? data.StatusLine.default : { modules: [] },
powerline: data.StatusLine.powerline && typeof data.StatusLine.powerline === 'object' && Array.isArray(data.StatusLine.powerline.modules) ? data.StatusLine.powerline : { modules: [] }
} : {
enabled: false,
currentStyle: 'default',
default: { modules: [] },
powerline: { modules: [] }
},
Router: data.Router && typeof data.Router === 'object' ? { Router: data.Router && typeof data.Router === 'object' ? {
default: typeof data.Router.default === 'string' ? data.Router.default : '', default: typeof data.Router.default === 'string' ? data.Router.default : '',
background: typeof data.Router.background === 'string' ? data.Router.background : '', background: typeof data.Router.background === 'string' ? data.Router.background : '',
@@ -113,6 +124,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
PROXY_URL: '', PROXY_URL: '',
transformers: [], transformers: [],
Providers: [], Providers: [],
StatusLine: undefined,
Router: { Router: {
default: '', default: '',
background: '', background: '',

View File

@@ -1,4 +1,3 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Dialog, Dialog,
@@ -13,6 +12,9 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Combobox } from "@/components/ui/combobox"; import { Combobox } from "@/components/ui/combobox";
import { useConfig } from "./ConfigProvider"; import { useConfig } from "./ConfigProvider";
import { StatusLineConfigDialog } from "./StatusLineConfigDialog";
import { useState } from "react";
import type { StatusLineConfig } from "@/types";
interface SettingsDialogProps { interface SettingsDialogProps {
isOpen: boolean; isOpen: boolean;
@@ -22,6 +24,7 @@ interface SettingsDialogProps {
export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) { export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const [isStatusLineConfigOpen, setIsStatusLineConfigOpen] = useState(false);
if (!config) { if (!config) {
return null; return null;
@@ -35,16 +38,71 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
setConfig({ ...config, CLAUDE_PATH: e.target.value }); setConfig({ ...config, CLAUDE_PATH: e.target.value });
}; };
const handleStatusLineEnabledChange = (checked: boolean) => {
// Ensure we have a complete StatusLineConfig object
const newStatusLineConfig: StatusLineConfig = {
enabled: checked,
currentStyle: config.StatusLine?.currentStyle || "default",
default: config.StatusLine?.default || { modules: [] },
powerline: config.StatusLine?.powerline || { modules: [] },
};
setConfig({
...config,
StatusLine: newStatusLineConfig,
});
};
const openStatusLineConfig = () => {
setIsStatusLineConfigOpen(true);
};
return ( return (
<Dialog open={isOpen} onOpenChange={onOpenChange}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent data-testid="settings-dialog">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("toplevel.title")}</DialogTitle> <DialogTitle>{t("toplevel.title")}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch id="log" checked={config.LOG} onCheckedChange={handleLogChange} /> <Switch
<Label htmlFor="log" className="transition-all-ease hover:scale-[1.02] cursor-pointer">{t("toplevel.log")}</Label> id="log"
checked={config.LOG}
onCheckedChange={handleLogChange}
/>
<Label
htmlFor="log"
className="transition-all-ease hover:scale-[1.02] cursor-pointer"
>
{t("toplevel.log")}
</Label>
</div>
{/* StatusLine Configuration */}
<div className="space-y-2 border-t pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Switch
id="statusline"
checked={config.StatusLine?.enabled || false}
onCheckedChange={handleStatusLineEnabledChange}
/>
<Label
htmlFor="statusline"
className="transition-all-ease hover:scale-[1.02] cursor-pointer"
>
{t("statusline.title")}
</Label>
</div>
<Button
variant="outline"
size="sm"
onClick={openStatusLineConfig}
className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]"
data-testid="statusline-config-button"
>
{t("app.settings")}
</Button>
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="log-level" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.log_level")}</Label> <Label htmlFor="log-level" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.log_level")}</Label>
@@ -62,34 +120,114 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="claude-path" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.claude_path")}</Label> <Label
<Input id="claude-path" value={config.CLAUDE_PATH} onChange={handlePathChange} className="transition-all-ease focus:scale-[1.01]" /> htmlFor="claude-path"
className="transition-all-ease hover:scale-[1.01] cursor-pointer"
>
{t("toplevel.claude_path")}
</Label>
<Input
id="claude-path"
value={config.CLAUDE_PATH}
onChange={handlePathChange}
className="transition-all-ease focus:scale-[1.01]"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="host" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.host")}</Label> <Label
<Input id="host" value={config.HOST} onChange={(e) => setConfig({ ...config, HOST: e.target.value })} className="transition-all-ease focus:scale-[1.01]" /> htmlFor="host"
className="transition-all-ease hover:scale-[1.01] cursor-pointer"
>
{t("toplevel.host")}
</Label>
<Input
id="host"
value={config.HOST}
onChange={(e) => setConfig({ ...config, HOST: e.target.value })}
className="transition-all-ease focus:scale-[1.01]"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="port" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.port")}</Label> <Label
<Input id="port" type="number" value={config.PORT} onChange={(e) => setConfig({ ...config, PORT: parseInt(e.target.value, 10) })} className="transition-all-ease focus:scale-[1.01]" /> htmlFor="port"
className="transition-all-ease hover:scale-[1.01] cursor-pointer"
>
{t("toplevel.port")}
</Label>
<Input
id="port"
type="number"
value={config.PORT}
onChange={(e) =>
setConfig({ ...config, PORT: parseInt(e.target.value, 10) })
}
className="transition-all-ease focus:scale-[1.01]"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="timeout" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.timeout")}</Label> <Label
<Input id="timeout" value={config.API_TIMEOUT_MS} onChange={(e) => setConfig({ ...config, API_TIMEOUT_MS: e.target.value })} className="transition-all-ease focus:scale-[1.01]" /> htmlFor="timeout"
className="transition-all-ease hover:scale-[1.01] cursor-pointer"
>
{t("toplevel.timeout")}
</Label>
<Input
id="timeout"
value={config.API_TIMEOUT_MS}
onChange={(e) =>
setConfig({ ...config, API_TIMEOUT_MS: e.target.value })
}
className="transition-all-ease focus:scale-[1.01]"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="proxy-url" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.proxy_url")}</Label> <Label
<Input id="proxy-url" value={config.PROXY_URL} onChange={(e) => setConfig({ ...config, PROXY_URL: e.target.value })} placeholder="http://127.0.0.1:7890" className="transition-all-ease focus:scale-[1.01]" /> htmlFor="proxy-url"
className="transition-all-ease hover:scale-[1.01] cursor-pointer"
>
{t("toplevel.proxy_url")}
</Label>
<Input
id="proxy-url"
value={config.PROXY_URL}
onChange={(e) =>
setConfig({ ...config, PROXY_URL: e.target.value })
}
placeholder="http://127.0.0.1:7890"
className="transition-all-ease focus:scale-[1.01]"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="apikey" className="transition-all-ease hover:scale-[1.01] cursor-pointer">{t("toplevel.apikey")}</Label> <Label
<Input id="apikey" type="password" value={config.APIKEY} onChange={(e) => setConfig({ ...config, APIKEY: e.target.value })} className="transition-all-ease focus:scale-[1.01]" /> htmlFor="apikey"
className="transition-all-ease hover:scale-[1.01] cursor-pointer"
>
{t("toplevel.apikey")}
</Label>
<Input
id="apikey"
type="password"
value={config.APIKEY}
onChange={(e) => setConfig({ ...config, APIKEY: e.target.value })}
className="transition-all-ease focus:scale-[1.01]"
/>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={() => onOpenChange(false)} className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">{t("app.save")}</Button> <Button
onClick={() => onOpenChange(false)}
className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]"
>
{t("app.save")}
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
<StatusLineConfigDialog
isOpen={isStatusLineConfigOpen}
onOpenChange={setIsStatusLineConfigOpen}
data-testid="statusline-config-dialog"
/>
</Dialog> </Dialog>
); );
} }

View File

@@ -0,0 +1,647 @@
import { useTranslation } from "react-i18next";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Combobox } from "@/components/ui/combobox";
import { ColorPicker } from "@/components/ui/color-picker";
import { Badge } from "@/components/ui/badge";
import { useConfig } from "./ConfigProvider";
import { validateStatusLineConfig, formatValidationError, createDefaultStatusLineConfig } from "@/utils/statusline";
import type { StatusLineConfig, StatusLineModuleConfig, StatusLineThemeConfig } from "@/types";
const DEFAULT_MODULE: StatusLineModuleConfig = {
type: "workDir",
icon: "󰉋",
text: "{{workDirName}}",
color: "bright_blue"
};
// 模块类型选项
const MODULE_TYPES = [
{ label: "workDir", value: "workDir" },
{ label: "gitBranch", value: "gitBranch" },
{ label: "model", value: "model" },
{ label: "usage", value: "usage" }
];
// ANSI颜色代码映射
const ANSI_COLORS: Record<string, string> = {
// 标准颜色
black: "text-black",
red: "text-red-600",
green: "text-green-600",
yellow: "text-yellow-500",
blue: "text-blue-500",
magenta: "text-purple-500",
cyan: "text-cyan-500",
white: "text-white",
// 亮色
bright_black: "text-gray-500",
bright_red: "text-red-400",
bright_green: "text-green-400",
bright_yellow: "text-yellow-300",
bright_blue: "text-blue-300",
bright_magenta: "text-purple-300",
bright_cyan: "text-cyan-300",
bright_white: "text-white",
// 背景颜色
bg_black: "bg-black",
bg_red: "bg-red-600",
bg_green: "bg-green-600",
bg_yellow: "bg-yellow-500",
bg_blue: "bg-blue-500",
bg_magenta: "bg-purple-500",
bg_cyan: "bg-cyan-500",
bg_white: "bg-white",
// 亮背景色
bg_bright_black: "bg-gray-800",
bg_bright_red: "bg-red-400",
bg_bright_green: "bg-green-400",
bg_bright_yellow: "bg-yellow-300",
bg_bright_blue: "bg-blue-300",
bg_bright_magenta: "bg-purple-300",
bg_bright_cyan: "bg-cyan-300",
bg_bright_white: "bg-gray-100",
// Powerline样式需要的额外背景色
bg_bright_orange: "bg-orange-400",
bg_bright_purple: "bg-purple-400",
};
// 变量替换函数
function replaceVariables(text: string, variables: Record<string, string>): string {
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] || match;
});
}
// 渲染单个模块预览
function renderModulePreview(module: StatusLineModuleConfig, isPowerline: boolean = false): React.ReactNode {
// 模拟变量数据
const variables = {
workDirName: "project",
gitBranch: "main",
model: "Claude Sonnet 4",
inputTokens: "1.2k",
outputTokens: "2.5k"
};
const text = replaceVariables(module.text, variables);
const icon = module.icon || "";
// 如果text为空且不是usage类型则跳过该模块
if (!text && module.type !== "usage") {
return null;
}
// 如果是Powerline样式添加背景色和分隔符
if (isPowerline) {
const bgColorClass = module.background ? ANSI_COLORS[module.background] || "" : "";
const textColorClass = module.color ? ANSI_COLORS[module.color] || "text-white" : "text-white";
return (
<div className={`powerline-module ${bgColorClass} ${textColorClass}`}>
<div className="powerline-module-content">
{icon && <span>{icon}</span>}
<span>{text}</span>
</div>
<div
className="powerline-separator"
data-current-bg={module.background || ""}
/>
</div>
);
}
return (
<>
{icon && <span>{icon}</span>}
<span>{text}</span>
</>
);
}
interface StatusLineConfigDialogProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
}
export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfigDialogProps) {
const { t } = useTranslation();
const { config, setConfig } = useConfig();
// 添加Powerline分隔符样式
useEffect(() => {
const styleElement = document.createElement('style');
styleElement.innerHTML = `
.powerline-module {
display: inline-flex;
align-items: center;
height: 28px;
position: relative;
padding: 0 8px;
overflow: visible;
}
.powerline-module-content {
display: flex;
align-items: center;
gap: 4px;
position: relative;
}
.powerline-separator {
width: 0;
height: 0;
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: 8px solid;
position: absolute;
right: -8px;
top: 0;
display: block;
}
/* 使用层级确保每个模块的三角形覆盖在下一个模块上方 */
.cursor-pointer:nth-child(1) .powerline-separator { z-index: 10; }
.cursor-pointer:nth-child(2) .powerline-separator { z-index: 9; }
.cursor-pointer:nth-child(3) .powerline-separator { z-index: 8; }
.cursor-pointer:nth-child(4) .powerline-separator { z-index: 7; }
.cursor-pointer:nth-child(5) .powerline-separator { z-index: 6; }
.cursor-pointer:nth-child(6) .powerline-separator { z-index: 5; }
.cursor-pointer:nth-child(7) .powerline-separator { z-index: 4; }
.cursor-pointer:nth-child(8) .powerline-separator { z-index: 3; }
.cursor-pointer:nth-child(9) .powerline-separator { z-index: 2; }
.cursor-pointer:nth-child(10) .powerline-separator { z-index: 1; }
.cursor-pointer:last-child .powerline-separator {
display: none;
}
/* 根据data属性动态设置颜色确保与模块背景色一致 */
.powerline-separator[data-current-bg="bg_black"] { border-left-color: #000000; }
.powerline-separator[data-current-bg="bg_red"] { border-left-color: #dc2626; }
.powerline-separator[data-current-bg="bg_green"] { border-left-color: #16a34a; }
.powerline-separator[data-current-bg="bg_yellow"] { border-left-color: #eab308; }
.powerline-separator[data-current-bg="bg_blue"] { border-left-color: #3b82f6; }
.powerline-separator[data-current-bg="bg_magenta"] { border-left-color: #a855f7; }
.powerline-separator[data-current-bg="bg_cyan"] { border-left-color: #06b6d4; }
.powerline-separator[data-current-bg="bg_white"] { border-left-color: #ffffff; }
.powerline-separator[data-current-bg="bg_bright_black"] { border-left-color: #1f2937; }
.powerline-separator[data-current-bg="bg_bright_red"] { border-left-color: #f87171; }
.powerline-separator[data-current-bg="bg_bright_green"] { border-left-color: #4ade80; }
.powerline-separator[data-current-bg="bg_bright_yellow"] { border-left-color: #fde047; }
.powerline-separator[data-current-bg="bg_bright_blue"] { border-left-color: #93c5fd; }
.powerline-separator[data-current-bg="bg_bright_magenta"] { border-left-color: #c084fc; }
.powerline-separator[data-current-bg="bg_bright_cyan"] { border-left-color: #22d3ee; }
.powerline-separator[data-current-bg="bg_bright_white"] { border-left-color: #f3f4f6; }
.powerline-separator[data-current-bg="bg_bright_orange"] { border-left-color: #fb923c; }
.powerline-separator[data-current-bg="bg_bright_purple"] { border-left-color: #c084fc; }
`;
document.head.appendChild(styleElement);
// 清理函数
return () => {
document.head.removeChild(styleElement);
};
}, []);
const [statusLineConfig, setStatusLineConfig] = useState<StatusLineConfig>(
config?.StatusLine || createDefaultStatusLineConfig()
);
const [selectedModuleIndex, setSelectedModuleIndex] = useState<number | null>(null);
// 模块类型选项
const MODULE_TYPES_OPTIONS = MODULE_TYPES.map(item => ({
...item,
label: t(`statusline.${item.label}`)
}));
const handleThemeChange = (value: string) => {
setStatusLineConfig(prev => ({ ...prev, currentStyle: value }));
};
const handleModuleChange = (index: number, field: keyof StatusLineModuleConfig, value: string) => {
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
if (modules[index]) {
modules[index] = { ...modules[index], [field]: value };
}
setStatusLineConfig(prev => ({
...prev,
[currentTheme]: { modules }
}));
};
const [validationErrors, setValidationErrors] = useState<string[]>([]);
const handleSave = () => {
// 验证配置
const validationResult = validateStatusLineConfig(statusLineConfig);
if (!validationResult.isValid) {
// 格式化错误信息
const errorMessages = validationResult.errors.map(error =>
formatValidationError(error, t)
);
setValidationErrors(errorMessages);
return;
}
// 清除之前的错误
setValidationErrors([]);
if (config) {
setConfig({
...config,
StatusLine: statusLineConfig
});
onOpenChange(false);
}
};
// 创建自定义Alert组件
const CustomAlert = ({
title,
description,
variant = "default"
}: {
title: string;
description: React.ReactNode;
variant?: "default" | "destructive";
}) => {
const isError = variant === "destructive";
return (
<div className={`rounded-lg border p-4 ${
isError
? "bg-red-50 border-red-200 text-red-800"
: "bg-blue-50 border-blue-200 text-blue-800"
}`}>
<div className="flex">
<div className="flex-shrink-0">
{isError ? (
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
) : (
<svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
)}
</div>
<div className="ml-3">
<h3 className={`text-sm font-medium ${
isError ? "text-red-800" : "text-blue-800"
}`}>
{title}
</h3>
<div className={`mt-2 text-sm ${
isError ? "text-red-700" : "text-blue-700"
}`}>
{description}
</div>
</div>
</div>
</div>
);
};
const currentThemeKey = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentThemeConfig = statusLineConfig[currentThemeKey];
const currentModules = currentThemeConfig && typeof currentThemeConfig === 'object' && 'modules' in currentThemeConfig
? ((currentThemeConfig as StatusLineThemeConfig).modules || [])
: [];
const selectedModule = selectedModuleIndex !== null && currentModules.length > selectedModuleIndex ? currentModules[selectedModuleIndex] : null;
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl h-[90vh] overflow-hidden sm:max-w-5xl md:max-w-6xl lg:max-w-7xl animate-in fade-in-90 slide-in-from-bottom-10 duration-300 flex flex-col">
<DialogHeader data-testid="statusline-config-dialog-header" className="border-b pb-4">
<DialogTitle className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M14 3v4a2 2 0 0 0 2 2h4"/>
<path d="M3 12h18"/>
</svg>
{t("statusline.title")}
</DialogTitle>
</DialogHeader>
{/* 错误显示区域 */}
{validationErrors.length > 0 && (
<div className="px-6">
<CustomAlert
variant="destructive"
title="配置验证失败"
description={
<ul className="list-disc pl-5 space-y-1">
{validationErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
}
/>
</div>
)}
<div className="flex flex-col gap-6 flex-1 overflow-hidden">
{/* 配置面板 */}
<div className="space-y-6">
{/* 主题样式选择 */}
<div className="flex items-center justify-between">
<Label htmlFor="theme-style" className="text-sm font-medium">
</Label>
<div className="w-1/2">
<Combobox
options={[
{ label: "默认", value: "default" },
{ label: "Powerline", value: "powerline" }
]}
value={statusLineConfig.currentStyle}
onChange={handleThemeChange}
data-testid="theme-selector"
placeholder="选择主题样式"
/>
</div>
</div>
</div>
{/* 三栏布局:组件列表 | 预览区域 | 属性配置 */}
<div className="grid grid-cols-5 gap-6 overflow-hidden flex-1">
{/* 左侧:支持的组件 */}
<div className="border rounded-lg p-4 flex flex-col overflow-hidden col-span-1">
<h3 className="text-sm font-medium mb-3"></h3>
<div className="space-y-2 overflow-y-auto flex-1">
{MODULE_TYPES_OPTIONS.map((moduleType) => (
<div
key={moduleType.value}
className="flex items-center gap-2 p-2 border rounded cursor-move hover:bg-secondary"
draggable
onDragStart={(e) => {
e.dataTransfer.setData("moduleType", moduleType.value);
}}
>
<span className="text-sm">{moduleType.label}</span>
</div>
))}
</div>
</div>
{/* 中间:预览区域 */}
<div className="border rounded-lg p-4 flex flex-col col-span-3">
<h3 className="text-sm font-medium mb-3"></h3>
<div
className={`rounded bg-black/90 text-white font-mono text-sm overflow-x-auto flex items-center border border-border p-3 py-5 shadow-inner ${statusLineConfig.currentStyle === 'powerline' ? 'gap-0 h-8 p-0 items-center overflow-visible relative' : 'h-5 overflow-hidden'}`}
data-testid="statusline-preview"
onDragOver={(e) => {
e.preventDefault();
}}
onDrop={(e) => {
e.preventDefault();
const moduleType = e.dataTransfer.getData("moduleType");
if (moduleType) {
// 添加新模块
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
// 根据模块类型设置默认值
let newModule: StatusLineModuleConfig;
switch (moduleType) {
case "workDir":
newModule = { type: "workDir", icon: "󰉋", text: "{{workDirName}}", color: "bright_blue" };
break;
case "gitBranch":
newModule = { type: "gitBranch", icon: "🌿", text: "{{gitBranch}}", color: "bright_green" };
break;
case "model":
newModule = { type: "model", icon: "🤖", text: "{{model}}", color: "bright_yellow" };
break;
case "usage":
newModule = { type: "usage", icon: "📊", text: "{{inputTokens}} → {{outputTokens}}", color: "bright_magenta" };
break;
default:
newModule = { ...DEFAULT_MODULE, type: moduleType };
}
modules.push(newModule);
setStatusLineConfig(prev => ({
...prev,
[currentTheme]: { modules }
}));
}
}}
>
{currentModules.length > 0 ? (
<div className="flex items-center flex-wrap gap-0">
{currentModules.map((module, index) => (
<div
key={index}
className={`cursor-pointer ${
selectedModuleIndex === index ? "bg-white/20" : "hover:bg-white/10"
} ${statusLineConfig.currentStyle === 'powerline' ? 'p-0 rounded-none inline-flex overflow-visible relative' : 'flex items-center gap-1 px-2 py-1 rounded'}`}
onClick={() => setSelectedModuleIndex(index)}
draggable
onDragStart={(e) => {
e.dataTransfer.setData("dragIndex", index.toString());
}}
onDragOver={(e) => {
e.preventDefault();
}}
onDrop={(e) => {
e.preventDefault();
const dragIndex = parseInt(e.dataTransfer.getData("dragIndex"));
if (!isNaN(dragIndex) && dragIndex !== index) {
// 重新排序模块
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
if (dragIndex >= 0 && dragIndex < modules.length && index >= 0 && index <= modules.length) {
const [movedModule] = modules.splice(dragIndex, 1);
modules.splice(index, 0, movedModule);
setStatusLineConfig(prev => ({
...prev,
[currentTheme]: { modules }
}));
// 更新选中项的索引
if (selectedModuleIndex === dragIndex) {
setSelectedModuleIndex(index);
} else if (selectedModuleIndex === index) {
setSelectedModuleIndex(dragIndex);
}
}
}
}}
>
{renderModulePreview(module, statusLineConfig.currentStyle === 'powerline')}
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center w-full py-4 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-gray-500 mb-2">
<path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"/>
<path d="M12 8v8"/>
<path d="M8 12h8"/>
</svg>
<span className="text-gray-500 text-sm">
</span>
</div>
)}
</div>
</div>
{/* 右侧:属性配置 */}
<div className="border rounded-lg p-4 flex flex-col overflow-hidden col-span-1">
<h3 className="text-sm font-medium mb-3"></h3>
<div className="overflow-y-auto flex-1">
{selectedModule && selectedModuleIndex !== null ? (
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("statusline.module_type")}</Label>
<Combobox
options={MODULE_TYPES_OPTIONS}
value={selectedModule.type}
onChange={(value) => handleModuleChange(selectedModuleIndex, "type", value)}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="module-icon">{t("statusline.module_icon")}</Label>
<Input
id="module-icon"
value={selectedModule.icon || ""}
onChange={(e) => handleModuleChange(selectedModuleIndex, "icon", e.target.value)}
placeholder="例如: 󰉋"
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="module-text">{t("statusline.module_text")}</Label>
<Input
id="module-text"
value={selectedModule.text}
onChange={(e) => handleModuleChange(selectedModuleIndex, "text", e.target.value)}
placeholder="例如: {{workDirName}}"
/>
<div className="text-xs text-muted-foreground">
<p>使:</p>
<div className="flex flex-wrap gap-1 mt-1">
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{workDirName}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{gitBranch}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{model}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{inputTokens}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{outputTokens}}"}</Badge>
</div>
</div>
</div>
<div className="space-y-2">
<Label>{t("statusline.module_color")}</Label>
<ColorPicker
value={selectedModule.color || ""}
onChange={(value) => handleModuleChange(selectedModuleIndex, "color", value)}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label>{t("statusline.module_background")}</Label>
<ColorPicker
value={selectedModule.background || ""}
onChange={(value) => handleModuleChange(selectedModuleIndex, "background", value)}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => {
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
modules.splice(selectedModuleIndex, 1);
setStatusLineConfig(prev => ({
...prev,
[currentTheme]: { modules }
}));
setSelectedModuleIndex(null);
}}
>
</Button>
</div>
) : (
<div className="flex items-center justify-center h-full min-h-[200px]">
<p className="text-muted-foreground text-sm"></p>
</div>
)}
</div>
</div>
</div>
</div>
<DialogFooter className="border-t pt-4 mt-4">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="transition-all hover:scale-105"
>
{t("app.cancel")}
</Button>
<Button
onClick={handleSave}
data-testid="save-statusline-config"
className="transition-all hover:scale-105"
>
{t("app.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,309 @@
import { useTranslation } from "react-i18next";
import React, { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { validateStatusLineConfig, backupConfig, restoreConfig, createDefaultStatusLineConfig } from "@/utils/statusline";
import type { StatusLineConfig } from "@/types";
interface StatusLineImportExportProps {
config: StatusLineConfig;
onImport: (config: StatusLineConfig) => void;
onShowToast: (message: string, type: 'success' | 'error' | 'warning') => void;
}
export function StatusLineImportExport({ config, onImport, onShowToast }: StatusLineImportExportProps) {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isImporting, setIsImporting] = useState(false);
// 导出配置为JSON文件
const handleExport = () => {
try {
// 在导出前验证配置
const validationResult = validateStatusLineConfig(config);
if (!validationResult.isValid) {
onShowToast(t("statusline.export_validation_failed"), 'error');
return;
}
const dataStr = JSON.stringify(config, null, 2);
const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`;
const exportFileDefaultName = `statusline-config-${new Date().toISOString().slice(0, 10)}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
onShowToast(t("statusline.export_success"), 'success');
} catch (error) {
console.error("Export failed:", error);
onShowToast(t("statusline.export_failed"), 'error');
}
};
// 导入配置从JSON文件
const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
setIsImporting(true);
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
const importedConfig = JSON.parse(content) as StatusLineConfig;
// 验证导入的配置
const validationResult = validateStatusLineConfig(importedConfig);
if (!validationResult.isValid) {
// 格式化错误信息
const errorMessages = validationResult.errors.map(error =>
error.message
).join('; ');
throw new Error(`${t("statusline.invalid_config")}: ${errorMessages}`);
}
onImport(importedConfig);
onShowToast(t("statusline.import_success"), 'success');
} catch (error) {
console.error("Import failed:", error);
onShowToast(t("statusline.import_failed") + (error instanceof Error ? `: ${error.message}` : ""), 'error');
} finally {
setIsImporting(false);
// 重置文件输入,以便可以再次选择同一个文件
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
reader.onerror = () => {
onShowToast(t("statusline.import_failed"), 'error');
setIsImporting(false);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
reader.readAsText(file);
};
// 下载配置模板
const handleDownloadTemplate = () => {
try {
// 使用新的默认配置函数
const templateConfig = createDefaultStatusLineConfig();
const dataStr = JSON.stringify(templateConfig, null, 2);
const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`;
const templateFileName = "statusline-config-template.json";
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', templateFileName);
linkElement.click();
onShowToast(t("statusline.template_download_success"), 'success');
} catch (error) {
console.error("Template download failed:", error);
onShowToast(t("statusline.template_download_failed"), 'error');
}
};
// 配置备份功能
const handleBackup = () => {
try {
const backupStr = backupConfig(config);
const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(backupStr)}`;
const backupFileName = `statusline-backup-${new Date().toISOString().slice(0, 10)}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', backupFileName);
linkElement.click();
onShowToast(t("statusline.backup_success"), 'success');
} catch (error) {
console.error("Backup failed:", error);
onShowToast(t("statusline.backup_failed"), 'error');
}
};
// 配置恢复功能
const handleRestore = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
const restoredConfig = restoreConfig(content);
if (!restoredConfig) {
throw new Error(t("statusline.invalid_backup_file"));
}
// 验证恢复的配置
const validationResult = validateStatusLineConfig(restoredConfig);
if (!validationResult.isValid) {
// 格式化错误信息
const errorMessages = validationResult.errors.map(error =>
error.message
).join('; ');
throw new Error(`${t("statusline.invalid_config")}: ${errorMessages}`);
}
onImport(restoredConfig);
onShowToast(t("statusline.restore_success"), 'success');
} catch (error) {
console.error("Restore failed:", error);
onShowToast(t("statusline.restore_failed") + (error instanceof Error ? `: ${error.message}` : ""), 'error');
} finally {
// 重置文件输入
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
reader.onerror = () => {
onShowToast(t("statusline.restore_failed"), 'error');
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
reader.readAsText(file);
};
// 移除本地验证函数因为我们现在使用utils中的验证函数
return (
<Card className="transition-all hover:shadow-md">
<CardHeader className="p-4">
<CardTitle className="text-lg flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
{t("statusline.import_export")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 px-4 pb-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="grid grid-cols-2 gap-3">
<Button
onClick={handleExport}
variant="outline"
className="transition-all hover:scale-105"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
{t("statusline.export")}
</Button>
<Button
onClick={() => fileInputRef.current?.click()}
variant="outline"
disabled={isImporting}
className="transition-all hover:scale-105"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
{t("statusline.import")}
</Button>
</div>
<div className="grid grid-cols-2 gap-3">
<Button
onClick={handleBackup}
variant="outline"
className="transition-all hover:scale-105"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
{t("statusline.backup")}
</Button>
<Button
onClick={() => {
// 创建一个隐藏的文件输入用于恢复
const restoreInput = document.createElement('input');
restoreInput.type = 'file';
restoreInput.accept = '.json';
restoreInput.onchange = (e) => handleRestore(e as any);
restoreInput.click();
}}
variant="outline"
className="transition-all hover:scale-105"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5"/>
</svg>
{t("statusline.restore")}
</Button>
</div>
<Button
onClick={handleDownloadTemplate}
variant="outline"
className="transition-all hover:scale-105 sm:col-span-2"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
{t("statusline.download_template")}
</Button>
</div>
<input
type="file"
ref={fileInputRef}
onChange={handleImport}
accept=".json"
className="hidden"
/>
<div className="p-3 bg-secondary/50 rounded-md">
<div className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-muted-foreground mt-0.5 flex-shrink-0">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
<div>
<p className="text-xs text-muted-foreground">
{t("statusline.import_export_help")}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,262 @@
"use client"
import * as React from "react"
import { HexColorPicker } from "react-colorful"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
interface ColorPickerProps {
value?: string;
onChange: (value: string) => void;
placeholder?: string;
showPreview?: boolean;
}
// 预定义的ANSI颜色映射
const ANSI_COLOR_MAP: Record<string, string> = {
"black": "#000000",
"red": "#ff0000",
"green": "#00ff00",
"yellow": "#ffff00",
"blue": "#0000ff",
"magenta": "#ff00ff",
"cyan": "#00ffff",
"white": "#ffffff",
"bright_black": "#808080",
"bright_red": "#ff8080",
"bright_green": "#80ff80",
"bright_yellow": "#ffff80",
"bright_blue": "#8080ff",
"bright_magenta": "#ff80ff",
"bright_cyan": "#80ffff",
"bright_white": "#ffffff"
}
// 背景颜色映射添加bg_前缀
const ANSI_BG_COLOR_MAP: Record<string, string> = Object.keys(ANSI_COLOR_MAP).reduce((acc, key) => {
acc[`bg_${key}`] = ANSI_COLOR_MAP[key]
return acc
}, {} as Record<string, string>)
// 合并所有颜色映射
const ALL_COLOR_MAP = { ...ANSI_COLOR_MAP, ...ANSI_BG_COLOR_MAP }
// 获取颜色值的函数
const getColorValue = (color: string): string => {
// 如果是预定义的ANSI颜色
if (ALL_COLOR_MAP[color]) {
return ALL_COLOR_MAP[color]
}
// 如果是十六进制颜色
if (color.startsWith("#")) {
return color
}
// 默认返回黑色
return "#000000"
}
export function ColorPicker({
value = "",
onChange,
placeholder = "选择颜色...",
showPreview = true
}: ColorPickerProps) {
const [open, setOpen] = React.useState(false)
const [customColor, setCustomColor] = React.useState("")
// 当value变化时更新customColor
React.useEffect(() => {
if (value.startsWith("#")) {
setCustomColor(value)
} else {
setCustomColor("")
}
}, [value])
const handleColorChange = (color: string) => {
onChange(color)
}
const handleCustomColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const color = e.target.value
setCustomColor(color)
// 验证十六进制颜色格式
if (/^#[0-9A-F]{6}$/i.test(color)) {
handleColorChange(color)
}
}
const handlePresetColorClick = (colorName: string) => {
handleColorChange(colorName)
setOpen(false)
}
const selectedColorValue = getColorValue(value)
// 获取ANSI颜色名称如果适用
const ansiColorName = Object.keys(ALL_COLOR_MAP).find(key => ALL_COLOR_MAP[key] === selectedColorValue) || value
return (
<div className="space-y-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal h-10 transition-all hover:scale-[1.02] active:scale-[0.98]",
!value && "text-muted-foreground"
)}
>
<div className="flex items-center gap-2 w-full">
{showPreview && (
<div
className="h-5 w-5 rounded border shadow-sm"
style={{ backgroundColor: selectedColorValue }}
/>
)}
<span className="truncate flex-1">
{value ? ansiColorName : placeholder}
</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m7 15 5 5 5-5"/>
<path d="m7 9 5-5 5 5"/>
</svg>
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-72 p-3" align="start">
<div className="space-y-4">
{/* 颜色选择器标题 */}
<div className="flex items-center justify-between">
<h4 className="text-sm font-semibold"></h4>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => handleColorChange("")}
>
</Button>
</div>
{/* 颜色预览 */}
<div className="flex items-center gap-2 p-2 rounded-md bg-secondary">
<div
className="h-8 w-8 rounded border shadow-sm"
style={{ backgroundColor: selectedColorValue }}
/>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{value ? ansiColorName : "未选择颜色"}
</div>
{value && value.startsWith("#") && (
<div className="text-xs text-muted-foreground font-mono">
{value.toUpperCase()}
</div>
)}
</div>
</div>
{/* 颜色选择器 */}
<div className="rounded-md overflow-hidden border">
<HexColorPicker
color={selectedColorValue}
onChange={handleColorChange}
className="w-full"
/>
</div>
{/* 自定义颜色输入 */}
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="flex gap-2">
<Input
type="text"
value={customColor}
onChange={handleCustomColorChange}
placeholder="#RRGGBB"
className="font-mono flex-1"
/>
<Button
size="sm"
onClick={() => customColor && handleColorChange(customColor)}
disabled={!customColor || !/^#[0-9A-F]{6}$/i.test(customColor)}
>
</Button>
</div>
<p className="text-xs text-muted-foreground">
(: #FF0000)
</p>
</div>
{/* 预定义颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">ANSI </label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
{/* 背景颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium"></label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_BG_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
)
}

View File

@@ -115,5 +115,55 @@
"cancel": "Cancel", "cancel": "Cancel",
"save_failed": "Failed to save config", "save_failed": "Failed to save config",
"save_and_restart": "Save & Restart" "save_and_restart": "Save & Restart"
},
"statusline": {
"title": "Status Line Configuration",
"enable": "Enable Status Line",
"theme": "Theme Style",
"theme_default": "Default",
"theme_powerline": "Powerline",
"modules": "Modules",
"module_type": "Type",
"module_icon": "Icon",
"module_text": "Text",
"module_color": "Color",
"module_background": "Background",
"add_module": "Add Module",
"remove_module": "Remove Module",
"preview": "Preview",
"workDir": "Working Directory",
"gitBranch": "Git Branch",
"model": "Model",
"usage": "Usage",
"background_none": "None",
"color_black": "Black",
"color_red": "Red",
"color_green": "Green",
"color_yellow": "Yellow",
"color_blue": "Blue",
"color_magenta": "Magenta",
"color_cyan": "Cyan",
"color_white": "White",
"color_bright_black": "Bright Black",
"color_bright_red": "Bright Red",
"color_bright_green": "Bright Green",
"color_bright_yellow": "Bright Yellow",
"color_bright_blue": "Bright Blue",
"color_bright_magenta": "Bright Magenta",
"color_bright_cyan": "Bright Cyan",
"color_bright_white": "Bright White",
"import_export": "Import/Export",
"import": "Import Config",
"export": "Export Config",
"download_template": "Download Template",
"import_export_help": "Export current configuration as a JSON file, or import configuration from a JSON file. You can also download a configuration template for reference.",
"export_success": "Configuration exported successfully",
"export_failed": "Failed to export configuration",
"import_success": "Configuration imported successfully",
"import_failed": "Failed to import configuration",
"invalid_config": "Invalid configuration file",
"template_download_success": "Template downloaded successfully",
"template_download_success_desc": "Configuration template has been downloaded to your device",
"template_download_failed": "Failed to download template"
} }
} }

View File

@@ -115,5 +115,55 @@
"cancel": "取消", "cancel": "取消",
"save_failed": "配置保存失败", "save_failed": "配置保存失败",
"save_and_restart": "保存并重启" "save_and_restart": "保存并重启"
},
"statusline": {
"title": "状态栏配置",
"enable": "启用状态栏",
"theme": "主题样式",
"theme_default": "默认",
"theme_powerline": "Powerline",
"modules": "模块",
"module_type": "类型",
"module_icon": "图标",
"module_text": "文本",
"module_color": "颜色",
"module_background": "背景",
"add_module": "添加模块",
"remove_module": "移除模块",
"preview": "预览",
"workDir": "工作目录",
"gitBranch": "Git分支",
"model": "模型",
"usage": "使用情况",
"background_none": "无",
"color_black": "黑色",
"color_red": "红色",
"color_green": "绿色",
"color_yellow": "黄色",
"color_blue": "蓝色",
"color_magenta": "品红",
"color_cyan": "青色",
"color_white": "白色",
"color_bright_black": "亮黑色",
"color_bright_red": "亮红色",
"color_bright_green": "亮绿色",
"color_bright_yellow": "亮黄色",
"color_bright_blue": "亮蓝色",
"color_bright_magenta": "亮品红",
"color_bright_cyan": "亮青色",
"color_bright_white": "亮白色",
"import_export": "导入/导出",
"import": "导入配置",
"export": "导出配置",
"download_template": "下载模板",
"import_export_help": "导出当前配置为JSON文件或从JSON文件导入配置。您也可以下载配置模板作为参考。",
"export_success": "配置导出成功",
"export_failed": "配置导出失败",
"import_success": "配置导入成功",
"import_failed": "配置导入失败",
"invalid_config": "无效的配置文件",
"template_download_success": "模板下载成功",
"template_download_success_desc": "配置模板已下载到您的设备",
"template_download_failed": "模板下载失败"
} }
} }

View File

@@ -27,10 +27,30 @@ export interface Transformer {
options?: Record<string, any>; options?: Record<string, any>;
} }
export interface StatusLineModuleConfig {
type: string;
icon?: string;
text: string;
color?: string;
background?: string;
}
export interface StatusLineThemeConfig {
modules: StatusLineModuleConfig[];
}
export interface StatusLineConfig {
enabled: boolean;
currentStyle: string;
default: StatusLineThemeConfig;
powerline: StatusLineThemeConfig;
}
export interface Config { export interface Config {
Providers: Provider[]; Providers: Provider[];
Router: RouterConfig; Router: RouterConfig;
transformers: Transformer[]; transformers: Transformer[];
StatusLine?: StatusLineConfig;
// Top-level settings // Top-level settings
LOG: boolean; LOG: boolean;
LOG_LEVEL: string; LOG_LEVEL: string;

146
ui/src/utils/statusline.ts Normal file
View File

@@ -0,0 +1,146 @@
import type { StatusLineConfig, StatusLineModuleConfig } from "@/types";
// 验证结果(保留接口但不使用)
export interface ValidationResult {
isValid: boolean;
errors: any[];
}
/**
* 验证StatusLine配置 - 已移除所有验证
* @param config 要验证的配置对象
* @returns 始终返回验证通过
*/
export function validateStatusLineConfig(config: unknown): ValidationResult {
// 不再执行任何验证
return { isValid: true, errors: [] };
}
/**
* 格式化错误信息(支持国际化)- 不再使用
*/
export function formatValidationError(error: unknown, t: (key: string, options?: Record<string, unknown>) => string): string {
return t("statusline.validation.unknown_error");
}
/**
* 解析颜色值,支持十六进制和内置颜色名称
* @param color 颜色值(可以是颜色名称或十六进制值)
* @param defaultColor 默认颜色(十六进制)
* @returns 十六进制颜色值
*/
export function parseColorValue(color: string | undefined, defaultColor: string = "#ffffff"): string {
if (!color) {
return defaultColor;
}
// 如果是十六进制颜色值(以#开头)
if (color.startsWith('#')) {
return color;
}
// 如果是已知的颜色名称,返回对应的十六进制值
return COLOR_HEX_MAP[color] || defaultColor;
}
/**
* 判断是否为有效的十六进制颜色值
* @param color 要检查的颜色值
* @returns 是否为有效的十六进制颜色值
*/
export function isHexColor(color: string): boolean {
return /^#([0-9A-F]{3}){1,2}$/i.test(color);
}
// 颜色枚举到十六进制的映射
export const COLOR_HEX_MAP: Record<string, string> = {
black: "#000000",
red: "#cd0000",
green: "#00cd00",
yellow: "#cdcd00",
blue: "#0000ee",
magenta: "#cd00cd",
cyan: "#00cdcd",
white: "#e5e5e5",
bright_black: "#7f7f7f",
bright_red: "#ff0000",
bright_green: "#00ff00",
bright_yellow: "#ffff00",
bright_blue: "#5c5cff",
bright_magenta: "#ff00ff",
bright_cyan: "#00ffff",
bright_white: "#ffffff",
bg_black: "#000000",
bg_red: "#cd0000",
bg_green: "#00cd00",
bg_yellow: "#cdcd00",
bg_blue: "#0000ee",
bg_magenta: "#cd00cd",
bg_cyan: "#00cdcd",
bg_white: "#e5e5e5",
bg_bright_black: "#7f7f7f",
bg_bright_red: "#ff0000",
bg_bright_green: "#00ff00",
bg_bright_yellow: "#ffff00",
bg_bright_blue: "#5c5cff",
bg_bright_magenta: "#ff00ff",
bg_bright_cyan: "#00ffff",
bg_bright_white: "#ffffff"
};
/**
* 创建默认的StatusLine配置
*/
export function createDefaultStatusLineConfig(): StatusLineConfig {
return {
enabled: false,
currentStyle: "default",
default: {
modules: [
{ type: "workDir", icon: "󰉋", text: "{{workDirName}}", color: "bright_blue" },
{ type: "gitBranch", icon: "", text: "{{gitBranch}}", color: "bright_magenta" },
{ type: "model", icon: "󰚩", text: "{{model}}", color: "bright_cyan" },
{ type: "usage", icon: "↑", text: "{{inputTokens}}", color: "bright_green" },
{ type: "usage", icon: "↓", text: "{{outputTokens}}", color: "bright_yellow" }
]
},
powerline: {
modules: [
{ type: "workDir", icon: "󰉋", text: "{{workDirName}}", color: "white", background: "bg_bright_blue" },
{ type: "gitBranch", icon: "", text: "{{gitBranch}}", color: "white", background: "bg_bright_magenta" },
{ type: "model", icon: "󰚩", text: "{{model}}", color: "white", background: "bg_bright_cyan" },
{ type: "usage", icon: "↑", text: "{{inputTokens}}", color: "white", background: "bg_bright_green" },
{ type: "usage", icon: "↓", text: "{{outputTokens}}", color: "white", background: "bg_bright_yellow" }
]
}
};
}
/**
* 创建配置备份
*/
export function backupConfig(config: StatusLineConfig): string {
const backup = {
config,
timestamp: new Date().toISOString(),
version: "1.0"
};
return JSON.stringify(backup, null, 2);
}
/**
* 从备份恢复配置
*/
export function restoreConfig(backupStr: string): StatusLineConfig | null {
try {
const backup = JSON.parse(backupStr);
if (backup && backup.config && backup.timestamp) {
return backup.config as StatusLineConfig;
}
return null;
} catch (error) {
console.error("Failed to restore config from backup:", error);
return null;
}
}

View File

@@ -1 +1 @@
{"root":["./src/App.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/ConfigProvider.tsx","./src/components/JsonEditor.tsx","./src/components/Login.tsx","./src/components/ProtectedRoute.tsx","./src/components/ProviderList.tsx","./src/components/Providers.tsx","./src/components/PublicRoute.tsx","./src/components/Router.tsx","./src/components/SettingsDialog.tsx","./src/components/TransformerList.tsx","./src/components/Transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"} {"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/pages/statuslinetestpage.tsx","./src/utils/statusline.ts"],"version":"5.8.3"}