add ccr statusline command
This commit is contained in:
@@ -41,3 +41,4 @@ This project is a TypeScript-based router for Claude Code requests. It allows ro
|
|||||||
- **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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一个自定义路由函数,用于根据请求确定使用哪个模型。
|
* 一个自定义路由函数,用于根据请求确定使用哪个模型。
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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
10
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
24
src/cli.ts
24
src/cli.ts
@@ -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
747
src/utils/statusline.ts
Normal 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
279
ui/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
240
ui/pnpm-lock.yaml
generated
@@ -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: {}
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
647
ui/src/components/StatusLineConfigDialog.tsx
Normal file
647
ui/src/components/StatusLineConfigDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
309
ui/src/components/StatusLineImportExport.tsx
Normal file
309
ui/src/components/StatusLineImportExport.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
262
ui/src/components/ui/color-picker.tsx
Normal file
262
ui/src/components/ui/color-picker.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "模板下载失败"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
146
ui/src/utils/statusline.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user