Compare commits
9 Commits
v0.2.0-bet
...
v0.2.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34636056da | ||
|
|
097cf40ccd | ||
|
|
88d2a03808 | ||
|
|
1830c18c7a | ||
|
|
cb8e3d0d26 | ||
|
|
8ea7418684 | ||
|
|
ec33ff4e4d | ||
|
|
22fb2823ce | ||
|
|
6dad84067d |
@@ -17,6 +17,11 @@ jobs:
|
||||
with:
|
||||
go-version: '1.24.3'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -27,9 +32,23 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Download dependencies
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: web/node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Download Go dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd web
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
@@ -49,7 +68,7 @@ jobs:
|
||||
echo "beta_num=${BETA_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "Building beta release: ${VERSION}"
|
||||
|
||||
- name: Build all platforms
|
||||
- name: Build (all platforms)
|
||||
run: |
|
||||
mkdir -p dist
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
|
||||
@@ -17,6 +17,11 @@ jobs:
|
||||
with:
|
||||
go-version: '1.24.3'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -27,9 +32,23 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: web/node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd web
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
@@ -45,7 +64,7 @@ jobs:
|
||||
echo "base=${BASE_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "Building stable release: ${VERSION}"
|
||||
|
||||
- name: Build all platforms
|
||||
- name: Build (all platforms)
|
||||
run: |
|
||||
mkdir -p dist
|
||||
LDFLAGS="-s -w"
|
||||
@@ -100,6 +119,9 @@ jobs:
|
||||
echo "| Windows x86_64 | [muyue-windows-amd64.zip](${DL_URL}/muyue-windows-amd64.zip) |"
|
||||
echo "| Windows ARM64 | [muyue-windows-arm64.zip](${DL_URL}/muyue-windows-arm64.zip) |"
|
||||
echo ""
|
||||
echo "The binary includes both CLI and Desktop modes."
|
||||
echo "Run \`muyue\` for TUI, \`muyue desktop\` for web UI."
|
||||
echo ""
|
||||
echo "### Install"
|
||||
echo ""
|
||||
echo "**Linux (x86_64)**"
|
||||
|
||||
@@ -15,6 +15,11 @@ jobs:
|
||||
with:
|
||||
go-version: '1.24.3'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -25,9 +30,23 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: web/node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd web
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,3 +28,6 @@ vendor/
|
||||
|
||||
# Config with secrets
|
||||
.muyue/
|
||||
|
||||
# Frontend (web/.gitignore handles specifics)
|
||||
web/node_modules/
|
||||
|
||||
152
CHANGELOG.md
152
CHANGELOG.md
@@ -4,6 +4,158 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## v0.2.1
|
||||
|
||||
### Changes since v0.2.1
|
||||
|
||||
- feat: complete TUI redesign with cyberpunk theme (#1) (cb8e3d0)
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | File |
|
||||
|----------|------|
|
||||
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-linux-amd64.tar.gz) |
|
||||
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-linux-arm64.tar.gz) |
|
||||
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-darwin-amd64.tar.gz) |
|
||||
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-darwin-arm64.tar.gz) |
|
||||
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-windows-amd64.zip) |
|
||||
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-windows-arm64.zip) |
|
||||
|
||||
### Install
|
||||
|
||||
**Linux (x86_64)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-linux-amd64.tar.gz | tar xz
|
||||
chmod +x muyue-linux-amd64
|
||||
sudo mv muyue-linux-amd64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**macOS (Apple Silicon)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-darwin-arm64.tar.gz | tar xz
|
||||
chmod +x muyue-darwin-arm64
|
||||
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**Windows (x86_64)**
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-windows-amd64.zip" -OutFile "muyue.zip"
|
||||
Expand-Archive -Path "muyue.zip" -DestinationPath "."
|
||||
Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe
|
||||
```
|
||||
|
||||
|
||||
## v0.2.1
|
||||
|
||||
### Changes since v0.2.0
|
||||
|
||||
- chore: bump version to 0.2.1, update README for TUI redesign (22fb282)
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | File |
|
||||
|----------|------|
|
||||
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-linux-amd64.tar.gz) |
|
||||
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-linux-arm64.tar.gz) |
|
||||
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-darwin-amd64.tar.gz) |
|
||||
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-darwin-arm64.tar.gz) |
|
||||
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-windows-amd64.zip) |
|
||||
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-windows-arm64.zip) |
|
||||
|
||||
### Install
|
||||
|
||||
**Linux (x86_64)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-linux-amd64.tar.gz | tar xz
|
||||
chmod +x muyue-linux-amd64
|
||||
sudo mv muyue-linux-amd64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**macOS (Apple Silicon)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-darwin-arm64.tar.gz | tar xz
|
||||
chmod +x muyue-darwin-arm64
|
||||
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**Windows (x86_64)**
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.1/muyue-windows-amd64.zip" -OutFile "muyue.zip"
|
||||
Expand-Archive -Path "muyue.zip" -DestinationPath "."
|
||||
Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe
|
||||
```
|
||||
|
||||
|
||||
## v0.2.0
|
||||
|
||||
### Changes since start
|
||||
|
||||
- refactor: redesign TUI with 4 tabs, red/rose theme, split layouts (035e923)
|
||||
- feat: GitFlow workflow with beta/stable CI pipelines (bbdac6c)
|
||||
- feat: security hardening, tests, doctor command, CI update, CHANGELOG (3494f6b)
|
||||
- refactor: modularize TUI, improve error handling, add CI caching and tests (4469122)
|
||||
- fix: remove tab switching, filter AI thinking from responses (5a33dfc)
|
||||
- fix: enable text selection, dashboard multi-column layout (82b2816)
|
||||
- feat: Ctrl+T tab switcher, minimal header, integrated terminal (2d6fc64)
|
||||
- feat: Ctrl+M tab switcher overlay menu (bb3b303)
|
||||
- fix: docker version check, uv PATH, install progress bar (e6fdec4)
|
||||
- feat: smart setup wizard - sort choices by system detection (1be4fc0)
|
||||
- fix: use Alt+1-5 for tab navigation to free number keys for input (825b429)
|
||||
- ci: add install instructions for all platforms in release body (ac35ff2)
|
||||
- ci: add build + release steps with push-only conditions (bcb9aa0)
|
||||
- ci: restore exact working ci.yml from e58e00d for testing (0a91cef)
|
||||
- fix: rename workflow back to CI (slash in name breaks Gitea 1.25) (461122a)
|
||||
- ci: trigger workflow run (ea59c2c)
|
||||
- fix: remove workflow_dispatch + add push-only conditions on release steps (9cd583f)
|
||||
- ci: single job - build + vet + release latest in one pass (92275be)
|
||||
- ci: merge CI and Release into single workflow (f2c0996)
|
||||
- fix: release workflow - delete old release before creating new one (5eb237f)
|
||||
- feat: redesign TUI + Ctrl+C quit confirm + version logic + sudo handling (e3cd618)
|
||||
- feat: add mouse support + install pnpm, uv, docker, gh (e58e00d)
|
||||
- fix: use GITEATOKEN secret name (no underscores in Gitea 1.25) (8e3f8b8)
|
||||
- fix: make release delete step resilient + check GITEA_TOKEN (69ca5c6)
|
||||
- fix: remove redundant newline in profiler.go (go vet) (2d421fe)
|
||||
- fix: export PATH in every step for Gitea runner compatibility (3f8e01f)
|
||||
- ci: restore actions/checkout + simplify workflows (4db69e4)
|
||||
- fix: add missing cmd/muyue/main.go and fix .gitignore (f650988)
|
||||
- ci: fix Gitea Actions - native checkout + auto-release on push (78c7239)
|
||||
- ci: migrate workflows to Gitea Actions with self-hosted runner (811a9aa)
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | File |
|
||||
|----------|------|
|
||||
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-linux-amd64.tar.gz) |
|
||||
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-linux-arm64.tar.gz) |
|
||||
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-darwin-amd64.tar.gz) |
|
||||
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-darwin-arm64.tar.gz) |
|
||||
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-windows-amd64.zip) |
|
||||
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-windows-arm64.zip) |
|
||||
|
||||
### Install
|
||||
|
||||
**Linux (x86_64)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-linux-amd64.tar.gz | tar xz
|
||||
chmod +x muyue-linux-amd64
|
||||
sudo mv muyue-linux-amd64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**macOS (Apple Silicon)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-darwin-arm64.tar.gz | tar xz
|
||||
chmod +x muyue-darwin-arm64
|
||||
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**Windows (x86_64)**
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.2.0/muyue-windows-amd64.zip" -OutFile "muyue.zip"
|
||||
Expand-Archive -Path "muyue.zip" -DestinationPath "."
|
||||
Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe
|
||||
```
|
||||
|
||||
|
||||
## [0.2.0] - 2026-04-20
|
||||
|
||||
### Added
|
||||
|
||||
20
Makefile
20
Makefile
@@ -3,10 +3,16 @@ GOBIN ?= $(GOPATH)/bin
|
||||
BINARY = muyue
|
||||
BUILD_DIR = .
|
||||
GO = go
|
||||
NODE ?= node
|
||||
NPM ?= npm
|
||||
WEB_DIR = web
|
||||
|
||||
.PHONY: build install clean test test-short run scan fmt lint build-all deps vet
|
||||
.PHONY: build install clean test test-short run scan fmt lint build-all deps vet frontend dev-desktop
|
||||
|
||||
build:
|
||||
frontend:
|
||||
cd $(WEB_DIR) && $(NPM) ci && $(NPM) run build
|
||||
|
||||
build: frontend
|
||||
$(GO) build -o $(BUILD_DIR)/$(BINARY) ./cmd/muyue/
|
||||
|
||||
install: build
|
||||
@@ -18,6 +24,8 @@ install-local: build
|
||||
|
||||
clean:
|
||||
rm -f $(BUILD_DIR)/$(BINARY)
|
||||
rm -rf $(WEB_DIR)/dist
|
||||
rm -rf $(WEB_DIR)/node_modules
|
||||
|
||||
test:
|
||||
$(GO) test ./... -v -count=1
|
||||
@@ -31,6 +39,12 @@ vet:
|
||||
run: build
|
||||
./$(BINARY)
|
||||
|
||||
desktop: build
|
||||
./$(BINARY) desktop
|
||||
|
||||
dev-desktop:
|
||||
cd $(WEB_DIR) && $(NPM) run dev
|
||||
|
||||
scan: build
|
||||
./$(BINARY) scan
|
||||
|
||||
@@ -41,7 +55,7 @@ fmt:
|
||||
lint:
|
||||
which golangci-lint > /dev/null 2>&1 && golangci-lint run || true
|
||||
|
||||
build-all:
|
||||
build-all: frontend
|
||||
GOOS=linux GOARCH=amd64 $(GO) build -o dist/$(BINARY)-linux-amd64 ./cmd/muyue/
|
||||
GOOS=linux GOARCH=arm64 $(GO) build -o dist/$(BINARY)-linux-arm64 ./cmd/muyue/
|
||||
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY)-darwin-amd64 ./cmd/muyue/
|
||||
|
||||
64
README.md
64
README.md
@@ -76,25 +76,49 @@ muyue skills deploy # Deploy skills to Crush and Claude Code
|
||||
muyue skills delete <name> # Delete a skill
|
||||
```
|
||||
|
||||
## TUI Controls
|
||||
## TUI — 4 Tabs
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Ctrl+T` | Open tab switcher |
|
||||
| `Tab` / `Shift+Tab` | Cycle tabs |
|
||||
| `Ctrl+C` | Quit confirmation |
|
||||
| `i` (Dashboard) | Install missing tools |
|
||||
| `u` (Dashboard) | Check for updates |
|
||||
| `s` (Dashboard) | Rescan system |
|
||||
| `a` (Workflow) | Approve plan |
|
||||
| `r` (Workflow) | Reject plan |
|
||||
| `g` (Workflow) | Generate plan |
|
||||
| `n` (Workflow) | Next step |
|
||||
| `x` (Workflow) | Cancel workflow |
|
||||
The TUI is organized into 4 tabs with a red/rose theme (`#E8364F` → `#FF6B8A`):
|
||||
|
||||
### Chat Commands
|
||||
### ◉ Dashboard
|
||||
|
||||
- `/plan <goal>` — Start a structured Plan→Execute workflow
|
||||
System overview: installed tools with status, active agents, updates, LSP/MCP/daemon status, and quick actions (install, update, scan).
|
||||
|
||||
### ◈ Studio
|
||||
|
||||
Central AI chat with a collapsible sidebar (`Ctrl+S`) containing 3 panels:
|
||||
|
||||
| Panel | Shortcut | Description |
|
||||
|-------|----------|-------------|
|
||||
| **Chat** | `1` | AI conversation, `/plan <goal>` to start workflows |
|
||||
| **Agents** | `2` | Start/stop Crush and Claude Code agents |
|
||||
| **Workflows** | `3` | Plan→Execute workflow controls (approve, reject, next step) |
|
||||
|
||||
### ▶ Shell
|
||||
|
||||
Split-view terminal with an AI assistant panel (`Ctrl+A` to toggle). The AI knows your system and suggests commands you can easily copy into the terminal.
|
||||
|
||||
### ⚙ Config
|
||||
|
||||
Profile, API providers, terminal/starship settings, BMAD, and skills — displayed in a two-column layout.
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Context | Action |
|
||||
|-----|---------|--------|
|
||||
| `Ctrl+T` | Global | Open tab switcher |
|
||||
| `Ctrl+S` | Studio | Toggle sidebar |
|
||||
| `Ctrl+A` | Shell | Toggle AI assistant panel |
|
||||
| `Ctrl+C` | Global | Quit confirmation |
|
||||
| `i` | Dashboard | Install missing tools |
|
||||
| `u` | Dashboard | Check for updates |
|
||||
| `s` | Dashboard | Rescan system |
|
||||
| `1` `2` `3` | Studio sidebar | Switch panels (Chat/Agents/Workflows) |
|
||||
| `a` | Workflow | Approve plan |
|
||||
| `r` | Workflow | Reject plan |
|
||||
| `g` | Workflow | Generate plan |
|
||||
| `n` | Workflow | Next step |
|
||||
| `x` | Workflow | Cancel workflow |
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -179,7 +203,7 @@ git push -u origin feature/my-feature
|
||||
|
||||
```bash
|
||||
# 1. Bump the version in internal/version/version.go
|
||||
# Change: Version = "0.2.0" → Version = "0.3.0"
|
||||
# Change: Version = "0.2.1" → Version = "0.3.0"
|
||||
# Commit on develop:
|
||||
git checkout develop
|
||||
# (edit internal/version/version.go)
|
||||
@@ -222,7 +246,7 @@ git push
|
||||
|
||||
```go
|
||||
const (
|
||||
Version = "0.2.0" // ← bump this before a release
|
||||
Version = "0.2.1" // ← bump this before a release
|
||||
)
|
||||
```
|
||||
|
||||
@@ -236,11 +260,11 @@ Binary version is injected at build time via `-ldflags`:
|
||||
```bash
|
||||
# Beta build (automatic in CI)
|
||||
go build -ldflags="-X github.com/muyue/muyue/internal/version.Prerelease=beta.3" ./cmd/muyue/
|
||||
# → muyue v0.2.0-beta.3
|
||||
# → muyue v0.2.1-beta.3
|
||||
|
||||
# Stable build (automatic in CI)
|
||||
go build -ldflags="-s -w" ./cmd/muyue/
|
||||
# → muyue v0.2.0
|
||||
# → muyue v0.2.1
|
||||
```
|
||||
|
||||
### Conventional commits
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/muyue/muyue/internal/config"
|
||||
"github.com/muyue/muyue/internal/desktop"
|
||||
"github.com/muyue/muyue/internal/installer"
|
||||
"github.com/muyue/muyue/internal/lsp"
|
||||
"github.com/muyue/muyue/internal/mcp"
|
||||
@@ -53,6 +54,8 @@ func handleCommand(args []string) {
|
||||
runLSP(args[1:])
|
||||
case "mcp":
|
||||
runMCP(args[1:])
|
||||
case "desktop":
|
||||
runDesktop(args[1:])
|
||||
case "skills":
|
||||
runSkills(args[1:])
|
||||
case "help", "-h", "--help":
|
||||
@@ -79,6 +82,7 @@ Commands:
|
||||
setup Run first-time setup wizard
|
||||
config Show current configuration
|
||||
doctor Check that everything is properly configured
|
||||
desktop Launch desktop web UI (opens browser)
|
||||
lsp [scan|install] Scan or install LSP servers
|
||||
mcp [config|scan] Configure MCP servers for Crush and Claude Code
|
||||
skills [list|generate|deploy|init|delete] Manage AI coding skills
|
||||
@@ -118,6 +122,14 @@ func runTUI() {
|
||||
}
|
||||
}
|
||||
|
||||
func runDesktop(args []string) {
|
||||
cfg := loadOrSetupConfig()
|
||||
if err := desktop.Run(cfg, args); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadOrSetupConfig() *config.MuyueConfig {
|
||||
if !config.Exists() {
|
||||
fmt.Println("First time setup detected!")
|
||||
|
||||
215
internal/api/handlers.go
Normal file
215
internal/api/handlers.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
|
||||
"github.com/muyue/muyue/internal/lsp"
|
||||
"github.com/muyue/muyue/internal/mcp"
|
||||
"github.com/muyue/muyue/internal/scanner"
|
||||
"github.com/muyue/muyue/internal/skills"
|
||||
"github.com/muyue/muyue/internal/updater"
|
||||
"github.com/muyue/muyue/internal/version"
|
||||
)
|
||||
|
||||
func writeJSON(w http.ResponseWriter, data interface{}) {
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, msg string, code int) {
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
||||
}
|
||||
|
||||
func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"name": version.Name,
|
||||
"version": version.Version,
|
||||
"author": version.Author,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) {
|
||||
if s.scanResult == nil {
|
||||
s.scanResult = scanner.ScanSystem()
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"system": s.scanResult.System,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleTools(w http.ResponseWriter, r *http.Request) {
|
||||
if s.scanResult == nil {
|
||||
s.scanResult = scanner.ScanSystem()
|
||||
}
|
||||
type toolInfo struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
Version string `json:"version"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
tools := make([]toolInfo, len(s.scanResult.Tools))
|
||||
for i, t := range s.scanResult.Tools {
|
||||
tools[i] = toolInfo{
|
||||
Name: t.Name,
|
||||
Installed: t.Installed,
|
||||
Version: t.Version,
|
||||
Path: t.Path,
|
||||
}
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"tools": tools,
|
||||
"total": len(tools),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if s.config == nil {
|
||||
writeError(w, "no config", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"profile": s.config.Profile,
|
||||
"terminal": s.config.Terminal,
|
||||
"bmad": s.config.BMAD,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) {
|
||||
if s.config == nil {
|
||||
writeError(w, "no config", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"providers": s.config.AI.Providers,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleSkills(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := skills.List()
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"skills": list,
|
||||
"count": len(list),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleLSP(w http.ResponseWriter, r *http.Request) {
|
||||
servers := lsp.ScanServers()
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"servers": servers,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) {
|
||||
servers := mcp.ScanServers()
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"servers": servers,
|
||||
"configured": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := mcp.ConfigureAll(s.config); err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdates(w http.ResponseWriter, r *http.Request) {
|
||||
result := scanner.ScanSystem()
|
||||
statuses := updater.CheckUpdates(result)
|
||||
type updateInfo struct {
|
||||
Tool string `json:"tool"`
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
NeedsUpdate bool `json:"needsUpdate"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
updates := make([]updateInfo, len(statuses))
|
||||
for i, u := range statuses {
|
||||
updates[i] = updateInfo{
|
||||
Tool: u.Tool,
|
||||
Current: u.Current,
|
||||
Latest: u.Latest,
|
||||
NeedsUpdate: u.NeedsUpdate,
|
||||
Error: u.Error,
|
||||
}
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"updates": updates,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Tools []string `json:"tools"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(body.Tools) == 0 {
|
||||
writeError(w, "no tools specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "installing"})
|
||||
}
|
||||
|
||||
func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
|
||||
s.scanResult = scanner.ScanSystem()
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Command string `json:"command"`
|
||||
Cwd string `json:"cwd"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Command == "" {
|
||||
writeError(w, "no command", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
shell := "/bin/sh"
|
||||
if s, err := exec.LookPath("bash"); err == nil {
|
||||
shell = s
|
||||
}
|
||||
|
||||
cmd := exec.Command(shell, "-c", body.Command)
|
||||
if body.Cwd != "" {
|
||||
cmd.Dir = body.Cwd
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
type termResult struct {
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
result := termResult{Output: string(out)}
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
writeJSON(w, result)
|
||||
}
|
||||
52
internal/api/server.go
Normal file
52
internal/api/server.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/muyue/muyue/internal/config"
|
||||
"github.com/muyue/muyue/internal/scanner"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
config *config.MuyueConfig
|
||||
scanResult *scanner.ScanResult
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.MuyueConfig) *Server {
|
||||
s := &Server{
|
||||
config: cfg,
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
s.scanResult = scanner.ScanSystem()
|
||||
s.routes()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) routes() {
|
||||
s.mux.HandleFunc("/api/info", s.handleInfo)
|
||||
s.mux.HandleFunc("/api/system", s.handleSystem)
|
||||
s.mux.HandleFunc("/api/tools", s.handleTools)
|
||||
s.mux.HandleFunc("/api/config", s.handleConfig)
|
||||
s.mux.HandleFunc("/api/providers", s.handleProviders)
|
||||
s.mux.HandleFunc("/api/skills", s.handleSkills)
|
||||
s.mux.HandleFunc("/api/lsp", s.handleLSP)
|
||||
s.mux.HandleFunc("/api/mcp", s.handleMCP)
|
||||
s.mux.HandleFunc("/api/updates", s.handleUpdates)
|
||||
s.mux.HandleFunc("/api/install", s.handleInstall)
|
||||
s.mux.HandleFunc("/api/scan", s.handleScan)
|
||||
s.mux.HandleFunc("/api/terminal", s.handleTerminal)
|
||||
s.mux.HandleFunc("/api/mcp/configure", s.handleMCPConfigure)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
131
internal/desktop/desktop.go
Normal file
131
internal/desktop/desktop.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/muyue/muyue/internal/api"
|
||||
"github.com/muyue/muyue/internal/config"
|
||||
"github.com/muyue/muyue/internal/version"
|
||||
"github.com/muyue/muyue/web"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
port int
|
||||
noOpen bool
|
||||
}
|
||||
|
||||
type option func(*options)
|
||||
|
||||
func WithPort(port int) option {
|
||||
return func(o *options) { o.port = port }
|
||||
}
|
||||
|
||||
func WithNoOpen(noOpen bool) option {
|
||||
return func(o *options) { o.noOpen = noOpen }
|
||||
}
|
||||
|
||||
func parseFlags(args []string) []option {
|
||||
var opts []option
|
||||
for _, arg := range args {
|
||||
switch {
|
||||
case arg == "--no-open":
|
||||
opts = append(opts, WithNoOpen(true))
|
||||
case strings.HasPrefix(arg, "--port="):
|
||||
if p, err := strconv.Atoi(strings.TrimPrefix(arg, "--port=")); err == nil {
|
||||
opts = append(opts, WithPort(p))
|
||||
}
|
||||
case arg == "--port":
|
||||
// handled as prefix case
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func Run(cfg *config.MuyueConfig, args []string) error {
|
||||
o := options{}
|
||||
for _, opt := range parseFlags(args) {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
log.Printf("%s Desktop v%s", version.Name, version.Version)
|
||||
|
||||
srv := api.NewServer(cfg)
|
||||
|
||||
frontendFS, err := fs.Sub(web.Assets, "dist")
|
||||
if err != nil {
|
||||
return fmt.Errorf("frontend assets: %w", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/api/", srv)
|
||||
mux.Handle("/", spaHandler(http.FileServer(http.FS(frontendFS))))
|
||||
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", o.port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bind %s: %w", addr, err)
|
||||
}
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
go func() {
|
||||
if err := http.Serve(listener, mux); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d", port)
|
||||
log.Printf("Listening on %s", url)
|
||||
|
||||
if !o.noOpen {
|
||||
openBrowser(url)
|
||||
log.Printf("Opened browser")
|
||||
}
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("Shutting down...")
|
||||
return nil
|
||||
}
|
||||
|
||||
func spaHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if path != "/" && !strings.Contains(path, ".") {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func openBrowser(url string) {
|
||||
var cmd *exec.Cmd
|
||||
switch {
|
||||
case exists("xdg-open"):
|
||||
cmd = exec.Command("xdg-open", url)
|
||||
case exists("open"):
|
||||
cmd = exec.Command("open", url)
|
||||
case exists("cmd"):
|
||||
cmd = exec.Command("cmd", "/c", "start", url)
|
||||
default:
|
||||
fmt.Printf("Open manually: %s\n", url)
|
||||
return
|
||||
}
|
||||
_ = cmd.Start()
|
||||
}
|
||||
|
||||
func exists(name string) bool {
|
||||
_, err := exec.LookPath(name)
|
||||
return err == nil
|
||||
}
|
||||
115
internal/tui/animations.go
Normal file
115
internal/tui/animations.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var glitchChars = "!@#$%^&*()_+-=[]{}|;':,./<>?~`"
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func randomGlitchChar() string {
|
||||
return string(glitchChars[rand.Intn(len(glitchChars))])
|
||||
}
|
||||
|
||||
func glitchText(text string, intensity int) string {
|
||||
runes := []rune(text)
|
||||
for i := 0; i < intensity; i++ {
|
||||
pos := rand.Intn(len(runes))
|
||||
if runes[pos] != ' ' && runes[pos] != '\n' {
|
||||
runes[pos] = []rune(randomGlitchChar())[0]
|
||||
}
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func generateScanLine(width int, frame int) string {
|
||||
pos := frame % width
|
||||
line := strings.Repeat(" ", pos) +
|
||||
lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(strings.Repeat("━", min(20, width-pos)))
|
||||
if pos+20 < width {
|
||||
line += strings.Repeat(" ", width-pos-20)
|
||||
}
|
||||
return line[:min(width, len(line))]
|
||||
}
|
||||
|
||||
func typewriterRender(text string, pos int) string {
|
||||
if pos >= len(text) {
|
||||
return text
|
||||
}
|
||||
if pos <= 0 {
|
||||
return ""
|
||||
}
|
||||
shown := text[:pos]
|
||||
cursor := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("█")
|
||||
return shown + cursor
|
||||
}
|
||||
|
||||
func renderGlitchEffect(width, height, frame int) string {
|
||||
var b strings.Builder
|
||||
for y := 0; y < height; y++ {
|
||||
line := ""
|
||||
for x := 0; x < width; x++ {
|
||||
if rand.Float64() < 0.15 {
|
||||
c := randomGlitchChar()
|
||||
style := lipgloss.NewStyle()
|
||||
r := rand.Float64()
|
||||
if r < 0.4 {
|
||||
style = style.Foreground(cyberRed)
|
||||
} else if r < 0.7 {
|
||||
style = style.Foreground(cyberPink)
|
||||
} else {
|
||||
style = style.Foreground(textMuted)
|
||||
}
|
||||
line += style.Render(c)
|
||||
} else {
|
||||
line += " "
|
||||
}
|
||||
}
|
||||
b.WriteString(line)
|
||||
if y < height-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func renderScanEffect(width, height, frame int) string {
|
||||
var b strings.Builder
|
||||
scanY := frame % (height + 10)
|
||||
for y := 0; y < height; y++ {
|
||||
if y == scanY || y == scanY+1 {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Render(strings.Repeat("━", width)))
|
||||
} else if y == scanY-1 || y == scanY+2 {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(strings.Repeat("─", width)))
|
||||
} else if y < scanY {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf("%*s", width, "")))
|
||||
} else {
|
||||
b.WriteString(strings.Repeat(" ", width))
|
||||
}
|
||||
if y < height-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func generateHexStream(width, lines int) string {
|
||||
var b strings.Builder
|
||||
for y := 0; y < lines; y++ {
|
||||
for x := 0; x < width/3; x++ {
|
||||
b.WriteString(fmt.Sprintf("%02X", rand.Intn(256)))
|
||||
}
|
||||
if y < lines-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return lipgloss.NewStyle().Foreground(dimRed).Render(b.String())
|
||||
}
|
||||
@@ -2,18 +2,11 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
||||
|
||||
func extractVersion(s string) string {
|
||||
return versionRegex.FindString(s)
|
||||
}
|
||||
|
||||
func (m Model) renderConfig() string {
|
||||
colWidth := m.width / 2
|
||||
if colWidth < 30 {
|
||||
@@ -22,7 +15,7 @@ func (m Model) renderConfig() string {
|
||||
|
||||
var left, right strings.Builder
|
||||
|
||||
left.WriteString(renderSectionWithIcon("Profile", "👤"))
|
||||
left.WriteString(renderSectionHeader("PROFILE", "[@]"))
|
||||
left.WriteString("\n")
|
||||
if m.config != nil {
|
||||
fields := []struct {
|
||||
@@ -50,28 +43,28 @@ func (m Model) renderConfig() string {
|
||||
}
|
||||
left.WriteString("\n")
|
||||
|
||||
left.WriteString(renderSectionWithIcon("AI Providers", "◆"))
|
||||
left.WriteString(renderSectionHeader("AI PROVIDERS", "[AI]"))
|
||||
left.WriteString("\n")
|
||||
if m.config != nil {
|
||||
for _, p := range m.config.AI.Providers {
|
||||
active := ""
|
||||
if p.Active {
|
||||
active = lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" ●")
|
||||
active = lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" >>")
|
||||
}
|
||||
keyStatus := itemMissingStyle.Render("no key")
|
||||
if p.APIKey != "" {
|
||||
keyStatus = itemOKStyle.Render("configured")
|
||||
}
|
||||
nameStyle := lipgloss.NewStyle().Foreground(textColor).Bold(true)
|
||||
nameStyle := lipgloss.NewStyle().Foreground(textBright).Bold(true)
|
||||
left.WriteString(fmt.Sprintf(" %s %s key=%s%s\n",
|
||||
nameStyle.Render(p.Name),
|
||||
lipgloss.NewStyle().Foreground(dimColor).Render("model="+p.Model),
|
||||
lipgloss.NewStyle().Foreground(dimRed).Render("model="+p.Model),
|
||||
keyStatus, active))
|
||||
}
|
||||
}
|
||||
left.WriteString("\n")
|
||||
|
||||
right.WriteString(renderSectionWithIcon("Terminal", "▶"))
|
||||
right.WriteString(renderSectionHeader("TERMINAL", "[$]"))
|
||||
right.WriteString("\n")
|
||||
if m.config != nil {
|
||||
right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Custom Prompt:"), valueStyle.Render(fmt.Sprintf("%v", m.config.Terminal.CustomPrompt))))
|
||||
@@ -81,12 +74,12 @@ func (m Model) renderConfig() string {
|
||||
}
|
||||
right.WriteString("\n")
|
||||
|
||||
right.WriteString(renderSectionWithIcon("BMAD Method", "◈"))
|
||||
right.WriteString(renderSectionHeader("BMAD METHOD", "[B]"))
|
||||
right.WriteString("\n")
|
||||
if m.config != nil {
|
||||
installed := itemMissingStyle.Render("no")
|
||||
installed := itemMissingStyle.Render("[--] no")
|
||||
if m.config.BMAD.Installed {
|
||||
installed = itemOKStyle.Render("yes")
|
||||
installed = itemOKStyle.Render("[OK] yes")
|
||||
}
|
||||
right.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Installed:"), installed))
|
||||
right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Global:"), valueStyle.Render(fmt.Sprintf("%v", m.config.BMAD.Global))))
|
||||
@@ -96,7 +89,7 @@ func (m Model) renderConfig() string {
|
||||
}
|
||||
right.WriteString("\n")
|
||||
|
||||
right.WriteString(renderSectionWithIcon(fmt.Sprintf("Skills (%d)", len(m.skillList)), "⚡"))
|
||||
right.WriteString(renderSectionHeader(fmt.Sprintf("SKILLS (%d)", len(m.skillList)), "[!]"))
|
||||
right.WriteString("\n")
|
||||
if len(m.skillList) > 0 {
|
||||
for _, s := range m.skillList {
|
||||
@@ -105,12 +98,12 @@ func (m Model) renderConfig() string {
|
||||
target = "both"
|
||||
}
|
||||
right.WriteString(fmt.Sprintf(" %s %s %s\n",
|
||||
lipgloss.NewStyle().Foreground(textColor).Render(s.Name),
|
||||
lipgloss.NewStyle().Foreground(primaryColor).Render("["+target+"]"),
|
||||
lipgloss.NewStyle().Foreground(dimColor).Render(s.Description)))
|
||||
lipgloss.NewStyle().Foreground(textMain).Render(s.Name),
|
||||
lipgloss.NewStyle().Foreground(cyberRed).Render("["+target+"]"),
|
||||
lipgloss.NewStyle().Foreground(dimRed).Render(s.Description)))
|
||||
}
|
||||
} else {
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(" No skills. Run `muyue skills init`."))
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(" No skills. Run `muyue skills init`."))
|
||||
right.WriteString("\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@ func (m Model) renderDashboard() string {
|
||||
|
||||
var left, right strings.Builder
|
||||
|
||||
left.WriteString(renderSectionWithIcon("System", "◉"))
|
||||
left.WriteString(renderSectionHeader("SYSTEM", "[*]"))
|
||||
left.WriteString("\n")
|
||||
if m.scanResult != nil {
|
||||
sysInfo := m.scanResult.System.String()
|
||||
left.WriteString(" ")
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(sysInfo))
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(sysInfo))
|
||||
}
|
||||
left.WriteString("\n\n")
|
||||
|
||||
left.WriteString(renderSectionWithIcon("Installed Tools", "◆"))
|
||||
left.WriteString(renderSectionHeader("INSTALLED TOOLS", "[+]"))
|
||||
left.WriteString("\n")
|
||||
if m.scanResult != nil {
|
||||
installed := 0
|
||||
@@ -33,14 +33,14 @@ func (m Model) renderDashboard() string {
|
||||
if t.Installed {
|
||||
installed++
|
||||
left.WriteString(" ")
|
||||
left.WriteString(itemOKStyle.Render("✓ "))
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(t.Name))
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s", extractVersion(t.Version))))
|
||||
left.WriteString(itemOKStyle.Render("[OK] "))
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(t.Name))
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s", extractVersion(t.Version))))
|
||||
left.WriteString("\n")
|
||||
} else {
|
||||
left.WriteString(" ")
|
||||
left.WriteString(itemMissingStyle.Render("✗ "))
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(t.Name))
|
||||
left.WriteString(itemMissingStyle.Render("[--] "))
|
||||
left.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(t.Name))
|
||||
left.WriteString(itemPendingStyle.Render(" (missing)"))
|
||||
left.WriteString("\n")
|
||||
}
|
||||
@@ -51,14 +51,14 @@ func (m Model) renderDashboard() string {
|
||||
if total > 0 {
|
||||
pct = (installed * barWidth) / total
|
||||
}
|
||||
bar := lipgloss.NewStyle().Foreground(primaryColor).Render(strings.Repeat("█", pct)) +
|
||||
lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("░", barWidth-pct))
|
||||
bar := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(strings.Repeat("█", pct)) +
|
||||
lipgloss.NewStyle().Foreground(dimRed).Render(strings.Repeat("░", barWidth-pct))
|
||||
left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total))
|
||||
}
|
||||
left.WriteString("\n")
|
||||
|
||||
if m.installing {
|
||||
left.WriteString(renderSectionWithIcon("Installing", "⏳"))
|
||||
left.WriteString(renderSectionHeader("INSTALLING", "[~]"))
|
||||
left.WriteString("\n")
|
||||
progBar := m.progressBar.View()
|
||||
label := ""
|
||||
@@ -72,7 +72,7 @@ func (m Model) renderDashboard() string {
|
||||
}
|
||||
|
||||
if len(m.installLog) > 0 {
|
||||
left.WriteString(renderSectionWithIcon("Install Log", "📋"))
|
||||
left.WriteString(renderSectionHeader("INSTALL LOG", "[#]"))
|
||||
left.WriteString("\n")
|
||||
for _, l := range m.installLog {
|
||||
left.WriteString(l + "\n")
|
||||
@@ -80,27 +80,27 @@ func (m Model) renderDashboard() string {
|
||||
left.WriteString("\n")
|
||||
}
|
||||
|
||||
right.WriteString(renderSectionWithIcon("Quick Actions", "⚡"))
|
||||
right.WriteString(renderSectionHeader("QUICK ACTIONS", "[!]"))
|
||||
right.WriteString("\n")
|
||||
actions := []struct {
|
||||
key string
|
||||
desc string
|
||||
color lipgloss.Color
|
||||
}{
|
||||
{"i", "Install missing tools", primaryColor},
|
||||
{"u", "Check for updates", warmColor},
|
||||
{"s", "Rescan system", roseColor},
|
||||
{"l", "Scan LSP servers", accentColor},
|
||||
{"m", "Configure MCP servers", roseLightColor},
|
||||
{"i", "Install missing tools", cyberRed},
|
||||
{"u", "Check for updates", neonRed},
|
||||
{"s", "Rescan system", cyberPink},
|
||||
{"l", "Scan LSP servers", cyberRose},
|
||||
{"m", "Configure MCP servers", brightRed},
|
||||
}
|
||||
for _, a := range actions {
|
||||
right.WriteString(fmt.Sprintf(" %s %s\n",
|
||||
lipgloss.NewStyle().Foreground(a.color).Bold(true).Render("["+a.key+"]"),
|
||||
lipgloss.NewStyle().Foreground(textColor).Render(a.desc)))
|
||||
lipgloss.NewStyle().Foreground(textMain).Render(a.desc)))
|
||||
}
|
||||
right.WriteString("\n")
|
||||
|
||||
right.WriteString(renderSectionWithIcon("Active Agents", "◉"))
|
||||
right.WriteString(renderSectionHeader("ACTIVE AGENTS", "[*]"))
|
||||
right.WriteString("\n")
|
||||
|
||||
agents := []struct {
|
||||
@@ -111,24 +111,24 @@ func (m Model) renderDashboard() string {
|
||||
}
|
||||
for _, a := range agents {
|
||||
right.WriteString(" ")
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("● "))
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(a.name + " "))
|
||||
right.WriteString(itemPendingStyle.Render("stopped"))
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(">> "))
|
||||
right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(a.name + " "))
|
||||
right.WriteString(itemPendingStyle.Render("[stopped]"))
|
||||
right.WriteString("\n")
|
||||
}
|
||||
right.WriteString("\n")
|
||||
|
||||
if len(m.updateStatus) > 0 {
|
||||
right.WriteString(renderSectionWithIcon("Updates", "↻"))
|
||||
right.WriteString(renderSectionHeader("UPDATES", "[^]"))
|
||||
right.WriteString("\n")
|
||||
for _, s := range m.updateStatus {
|
||||
if s.NeedsUpdate {
|
||||
right.WriteString(" ")
|
||||
right.WriteString(itemWarnStyle.Render("⚠ "))
|
||||
right.WriteString(fmt.Sprintf("%s: %s → %s\n", s.Tool, s.Current, s.Latest))
|
||||
right.WriteString(itemWarnStyle.Render("[!!] "))
|
||||
right.WriteString(fmt.Sprintf("%s: %s -> %s\n", s.Tool, s.Current, s.Latest))
|
||||
} else if s.Error == "" {
|
||||
right.WriteString(" ")
|
||||
right.WriteString(itemOKStyle.Render("✓ "))
|
||||
right.WriteString(itemOKStyle.Render("[OK] "))
|
||||
right.WriteString(fmt.Sprintf("%s: up to date\n", s.Tool))
|
||||
}
|
||||
}
|
||||
@@ -136,18 +136,18 @@ func (m Model) renderDashboard() string {
|
||||
}
|
||||
|
||||
if len(m.lspServers) > 0 {
|
||||
right.WriteString(renderSectionWithIcon("LSP Servers", "§"))
|
||||
right.WriteString(renderSectionHeader("LSP SERVERS", "[L]"))
|
||||
right.WriteString("\n")
|
||||
lspInstalled := 0
|
||||
for _, s := range m.lspServers {
|
||||
if s.Installed {
|
||||
lspInstalled++
|
||||
right.WriteString(" ")
|
||||
right.WriteString(itemOKStyle.Render("✓ "))
|
||||
right.WriteString(itemOKStyle.Render("[OK] "))
|
||||
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
|
||||
} else {
|
||||
right.WriteString(" ")
|
||||
right.WriteString(itemPendingStyle.Render("○ "))
|
||||
right.WriteString(itemPendingStyle.Render("[ ] "))
|
||||
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
|
||||
}
|
||||
}
|
||||
@@ -155,16 +155,16 @@ func (m Model) renderDashboard() string {
|
||||
right.WriteString("\n")
|
||||
}
|
||||
|
||||
mcpStatus := itemPendingStyle.Render("○ not configured")
|
||||
mcpStatus := itemPendingStyle.Render("[ ] not configured")
|
||||
if m.mcpConfigured {
|
||||
mcpStatus = itemOKStyle.Render("✓ configured")
|
||||
mcpStatus = itemOKStyle.Render("[OK] configured")
|
||||
}
|
||||
right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus))
|
||||
|
||||
if m.daemon != nil {
|
||||
daemonStatus := itemPendingStyle.Render("○ stopped")
|
||||
daemonStatus := itemPendingStyle.Render("[ ] stopped")
|
||||
if m.daemon.IsRunning() {
|
||||
daemonStatus = itemOKStyle.Render("✓ running")
|
||||
daemonStatus = itemOKStyle.Render("[OK] running")
|
||||
}
|
||||
right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus))
|
||||
}
|
||||
@@ -174,8 +174,3 @@ func (m Model) renderDashboard() string {
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol)
|
||||
}
|
||||
|
||||
func renderSectionWithIcon(title string, icon string) string {
|
||||
return lipgloss.NewStyle().Foreground(primaryColor).Render(icon+" ") +
|
||||
sectionStyle.Render(title)
|
||||
}
|
||||
|
||||
74
internal/tui/footer.go
Normal file
74
internal/tui/footer.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muyue/muyue/internal/version"
|
||||
)
|
||||
|
||||
func (m Model) renderFooter() string {
|
||||
profile := "unknown"
|
||||
if m.config != nil && m.config.Profile.Pseudo != "" {
|
||||
profile = m.config.Profile.Pseudo
|
||||
}
|
||||
|
||||
left := fmt.Sprintf(" %s@%s",
|
||||
lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(profile),
|
||||
lipgloss.NewStyle().Foreground(dimRed).Render(version.Name))
|
||||
leftR := statusBarStyle.Render(left)
|
||||
|
||||
var helpText string
|
||||
switch m.activeTab {
|
||||
case tabDashboard:
|
||||
helpText = "[i] install [u] update [s] scan [ctrl+t] tabs"
|
||||
case tabStudio:
|
||||
helpText = "[enter] send [ctrl+s] sidebar [ctrl+t] tabs"
|
||||
case tabShell:
|
||||
helpText = "[enter] run [ctrl+a] AI panel [ctrl+c] kill"
|
||||
case tabConfig:
|
||||
helpText = "[up/down] sections [ctrl+t] tabs"
|
||||
default:
|
||||
helpText = "[ctrl+t] tabs [ctrl+c] quit"
|
||||
}
|
||||
rightR := statusBarStyle.Render(helpText)
|
||||
|
||||
updateIndicator := ""
|
||||
if len(m.updateStatus) > 0 {
|
||||
needsUpdate := false
|
||||
for _, s := range m.updateStatus {
|
||||
if s.NeedsUpdate {
|
||||
needsUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if needsUpdate {
|
||||
updateIndicator = lipgloss.NewStyle().Foreground(warnAmber).Render(" [UPD] ")
|
||||
} else {
|
||||
updateIndicator = lipgloss.NewStyle().Foreground(successGreen).Render(" [OK] ")
|
||||
}
|
||||
}
|
||||
|
||||
verStr := lipgloss.NewStyle().Foreground(dimRed).Render("v" + version.Version)
|
||||
midContent := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render(
|
||||
updateIndicator + verStr,
|
||||
)
|
||||
|
||||
gap := m.width - lipgloss.Width(leftR) - lipgloss.Width(rightR) - lipgloss.Width(midContent)
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
|
||||
statusLine := lipgloss.JoinHorizontal(lipgloss.Bottom,
|
||||
leftR,
|
||||
strings.Repeat(" ", gap),
|
||||
midContent,
|
||||
rightR,
|
||||
)
|
||||
|
||||
helpLine := lipgloss.NewStyle().Background(bgSurface).Foreground(textMuted).Render(
|
||||
lipgloss.NewStyle().Padding(0, 1).Render(m.helpModel.View(keys)))
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, statusLine, helpLine)
|
||||
}
|
||||
@@ -142,16 +142,14 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
case "enter":
|
||||
m.activeTab = tab(m.tabMenuCursor)
|
||||
m.switchTab(tab(m.tabMenuCursor))
|
||||
m.showingTabMenu = false
|
||||
m.resizeViewport()
|
||||
return m, nil
|
||||
default:
|
||||
for i := 0; i < int(tabCount); i++ {
|
||||
if msg.String() == fmt.Sprintf("%d", i+1) {
|
||||
m.activeTab = tab(i)
|
||||
m.switchTab(tab(i))
|
||||
m.showingTabMenu = false
|
||||
m.resizeViewport()
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
@@ -159,6 +157,17 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) switchTab(t tab) {
|
||||
if t == m.activeTab {
|
||||
return
|
||||
}
|
||||
m.prevTab = m.activeTab
|
||||
m.activeTab = t
|
||||
m.transition = transitionGlitch
|
||||
m.transitionTick = 0
|
||||
m.resizeViewport()
|
||||
}
|
||||
|
||||
func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "i":
|
||||
@@ -174,13 +183,13 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
m.installLog = append(m.installLog, itemOKStyle.Render("✓ All tools already installed!"))
|
||||
m.installLog = append(m.installLog, itemOKStyle.Render("[OK] All tools already installed!"))
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
return m, nil
|
||||
}
|
||||
needsSudo := checkNeedsSudo(m.scanResult)
|
||||
if needsSudo && !hasSudo() {
|
||||
m.installLog = append(m.installLog, errMsgStyle.Render("✗ Some tools require sudo. Run: sudo muyue install"))
|
||||
m.installLog = append(m.installLog, errMsgStyle.Render("[!!] Some tools require sudo. Run: sudo muyue install"))
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
return m, nil
|
||||
}
|
||||
@@ -267,7 +276,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "a":
|
||||
if wf.Phase == workflow.PhaseReviewing {
|
||||
m.chatLog = append(m.chatLog, userMsgStyle.Render("⟩ [Plan approved]"))
|
||||
m.chatLog = append(m.chatLog, userMsgStyle.Render(">> [Plan approved]"))
|
||||
m.chatLoading = true
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
return m, reviewPlanCmd(m.orch, true, "")
|
||||
@@ -280,7 +289,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case "g":
|
||||
if wf.Phase == workflow.PhaseGathering && len(wf.Plan.Questions) > 0 && len(wf.Plan.Answers) >= len(wf.Plan.Questions) {
|
||||
m.chatLog = append(m.chatLog, userMsgStyle.Render("⟩ [Generate plan]"))
|
||||
m.chatLog = append(m.chatLog, userMsgStyle.Render(">> [Generate plan]"))
|
||||
m.chatLoading = true
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
return m, generatePlanCmd(m.orch)
|
||||
@@ -332,7 +341,7 @@ func hasSudo() bool {
|
||||
|
||||
func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) {
|
||||
input := m.chatInput
|
||||
m.chatLog = append(m.chatLog, userMsgStyle.Render("⟩ "+input))
|
||||
m.chatLog = append(m.chatLog, userMsgStyle.Render(">> "+input))
|
||||
m.chatInput = ""
|
||||
m.chatLoading = true
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
|
||||
178
internal/tui/header.go
Normal file
178
internal/tui/header.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muyue/muyue/internal/version"
|
||||
)
|
||||
|
||||
func (m Model) renderHeader() string {
|
||||
var tabs []string
|
||||
for i, name := range tabNames {
|
||||
icon := tabIcons[i]
|
||||
if tab(i) == m.activeTab {
|
||||
tabStyle := lipgloss.NewStyle().
|
||||
Background(cyberRed).
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
Padding(0, 2)
|
||||
tabs = append(tabs, tabStyle.Render(icon+" "+name))
|
||||
} else {
|
||||
tabStyle := lipgloss.NewStyle().
|
||||
Background(bgSurface).
|
||||
Foreground(textDim).
|
||||
Padding(0, 2)
|
||||
tabs = append(tabs, tabStyle.Render(icon+" "+name))
|
||||
}
|
||||
}
|
||||
|
||||
tabLine := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...))
|
||||
|
||||
timeStr := ""
|
||||
if !m.currentTime.IsZero() {
|
||||
timeStr = m.currentTime.Format("15:04:05")
|
||||
}
|
||||
|
||||
dateStr := ""
|
||||
if !m.currentTime.IsZero() {
|
||||
dateStr = m.currentTime.Format("02/01/2006")
|
||||
}
|
||||
|
||||
rightInfo := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render(
|
||||
lipgloss.JoinHorizontal(lipgloss.Center,
|
||||
lipgloss.NewStyle().Foreground(textDim).Render(dateStr+" "),
|
||||
lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(timeStr),
|
||||
lipgloss.NewStyle().Foreground(textMuted).Render(" "+getAnimFrame(m.animationFrame)),
|
||||
),
|
||||
)
|
||||
|
||||
statusDots := ""
|
||||
if m.config != nil {
|
||||
hasAI := false
|
||||
for _, p := range m.config.AI.Providers {
|
||||
if p.Active && p.APIKey != "" {
|
||||
hasAI = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasAI {
|
||||
statusDots += lipgloss.NewStyle().Foreground(successGreen).Render("●")
|
||||
} else {
|
||||
statusDots += lipgloss.NewStyle().Foreground(errorRed).Render("●")
|
||||
}
|
||||
} else {
|
||||
statusDots += lipgloss.NewStyle().Foreground(warnAmber).Render("●")
|
||||
}
|
||||
|
||||
statusDots += lipgloss.NewStyle().Foreground(textMuted).Render(" ")
|
||||
|
||||
if m.mcpConfigured {
|
||||
statusDots += lipgloss.NewStyle().Foreground(successGreen).Render("●")
|
||||
} else {
|
||||
statusDots += lipgloss.NewStyle().Foreground(warnAmber).Render("●")
|
||||
}
|
||||
|
||||
statusInfo := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render(
|
||||
lipgloss.JoinHorizontal(lipgloss.Center,
|
||||
lipgloss.NewStyle().Foreground(textDim).Render("SYS "),
|
||||
statusDots,
|
||||
),
|
||||
)
|
||||
|
||||
badge := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("MUYUE")
|
||||
versionBadge := lipgloss.NewStyle().Foreground(dimRed).Render("v" + version.Version)
|
||||
|
||||
logoLine := lipgloss.NewStyle().Background(bgVoid).Padding(0, 1).Render(
|
||||
lipgloss.JoinHorizontal(lipgloss.Center, badge, " ", versionBadge),
|
||||
)
|
||||
|
||||
topLine := lipgloss.JoinHorizontal(lipgloss.Bottom,
|
||||
logoLine,
|
||||
strings.Repeat(" ", max(0, m.width-lipgloss.Width(logoLine)-lipgloss.Width(rightInfo)-lipgloss.Width(statusInfo))),
|
||||
statusInfo,
|
||||
rightInfo,
|
||||
)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, topLine, tabLine)
|
||||
}
|
||||
|
||||
func (m Model) renderTabMenuOverlay() string {
|
||||
menuStyle := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(cyberRed).
|
||||
Background(bgCard).
|
||||
Padding(1, 3)
|
||||
|
||||
tabItemStyle := lipgloss.NewStyle().
|
||||
Foreground(textDim).
|
||||
Padding(0, 2)
|
||||
|
||||
tabItemActiveStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Background(cyberRed).
|
||||
Bold(true).
|
||||
Padding(0, 2)
|
||||
|
||||
descs := []string{
|
||||
"tools, updates & system status",
|
||||
"chat, agents & workflows",
|
||||
"terminal + AI assistant",
|
||||
"profile, API keys & settings",
|
||||
}
|
||||
|
||||
var items []string
|
||||
for i, name := range tabNames {
|
||||
num := lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %d.", i+1))
|
||||
icon := tabIcons[i] + " "
|
||||
if i == m.tabMenuCursor {
|
||||
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(cyberRose).Render(descs[i]))
|
||||
items = append(items, tabItemActiveStyle.Render(">"+item))
|
||||
} else {
|
||||
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(textMuted).Render(descs[i]))
|
||||
items = append(items, tabItemStyle.Render(" "+item))
|
||||
}
|
||||
}
|
||||
|
||||
header := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("SWITCH TAB")
|
||||
content := header + "\n\n" +
|
||||
strings.Join(items, "\n") +
|
||||
"\n\n" +
|
||||
lipgloss.NewStyle().Foreground(textMuted).Render("up/down navigate | enter select | esc cancel")
|
||||
|
||||
box := menuStyle.Render(content)
|
||||
|
||||
return lipgloss.Place(m.width, m.height,
|
||||
0.5, 0.5,
|
||||
box,
|
||||
lipgloss.WithWhitespaceBackground(bgVoid),
|
||||
lipgloss.WithWhitespaceForeground(textMuted),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) renderQuitOverlay() string {
|
||||
yesStyle := confirmNoStyle
|
||||
noStyle := confirmYesStyle
|
||||
if m.confirmCursor == 0 {
|
||||
yesStyle = confirmYesStyle
|
||||
noStyle = confirmNoStyle
|
||||
}
|
||||
|
||||
frame := lipgloss.NewStyle().Foreground(cyberRed).Render(getAnimFrame(m.animationFrame))
|
||||
|
||||
box := fmt.Sprintf("\n\n %s Quit muyue?\n\n %s %s",
|
||||
frame,
|
||||
yesStyle.Render("[ Yes ]"),
|
||||
noStyle.Render("[ No ]"),
|
||||
)
|
||||
|
||||
content := confirmBoxStyle.Render(box)
|
||||
|
||||
return lipgloss.Place(m.width, m.height,
|
||||
0.5, 0.5,
|
||||
content,
|
||||
lipgloss.WithWhitespaceBackground(bgVoid),
|
||||
lipgloss.WithWhitespaceForeground(textMuted),
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/muyue/muyue/internal/workflow"
|
||||
)
|
||||
|
||||
var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
||||
|
||||
func extractVersion(s string) string {
|
||||
return versionRegex.FindString(s)
|
||||
}
|
||||
|
||||
type previewFile = workflow.PreviewFile
|
||||
|
||||
func parsePreviewFiles(response string) []previewFile {
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/muyue/muyue/internal/proxy"
|
||||
"github.com/muyue/muyue/internal/scanner"
|
||||
"github.com/muyue/muyue/internal/skills"
|
||||
"github.com/muyue/muyue/internal/version"
|
||||
)
|
||||
|
||||
func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
||||
@@ -44,9 +43,9 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
||||
|
||||
sp := spinner.New()
|
||||
sp.Spinner = spinner.Dot
|
||||
sp.Style = lipgloss.NewStyle().Foreground(primaryColor)
|
||||
sp.Style = lipgloss.NewStyle().Foreground(cyberRed)
|
||||
|
||||
prog := progress.New(progress.WithGradient("#E8364F", "#FF6B8A"))
|
||||
prog := progress.New(progress.WithGradient("#FF0033", "#FF1A5E"))
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
@@ -55,8 +54,8 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
||||
scanResult: scan,
|
||||
activeTab: tabDashboard,
|
||||
chatLog: []string{
|
||||
aiMsgStyle.Render(" Welcome to Studio! Chat with your AI assistant here."),
|
||||
aiMsgStyle.Render(" Configure agents and workflows from the sidebar. Type /plan <goal> to start."),
|
||||
aiMsgStyle.Render(" >> Welcome to Studio! Chat with your AI assistant here."),
|
||||
aiMsgStyle.Render(" >> Configure agents and workflows from the sidebar. Type /plan <goal> to start."),
|
||||
},
|
||||
orch: orch,
|
||||
proxyMgr: proxyMgr,
|
||||
@@ -77,12 +76,14 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
||||
studioPanel: panelChat,
|
||||
studioSidebarOpen: true,
|
||||
termAIChat: []string{
|
||||
aiMsgStyle.Render(" I know your system inside out. Ask me anything."),
|
||||
aiMsgStyle.Render(" >> I know your system inside out. Ask me anything."),
|
||||
},
|
||||
termAIShow: true,
|
||||
configSection: configProfile,
|
||||
configField: 0,
|
||||
animationFrame: 0,
|
||||
currentTime: time.Now(),
|
||||
transition: transitionNone,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +93,14 @@ func animTick() tea.Cmd {
|
||||
})
|
||||
}
|
||||
|
||||
func clockTick() tea.Cmd {
|
||||
return tea.Tick(1*time.Second, func(t time.Time) tea.Msg {
|
||||
return clockTickMsg{time: t}
|
||||
})
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(spinner.Tick, animTick(), tea.EnterAltScreen)
|
||||
return tea.Batch(spinner.Tick, animTick(), clockTick(), tea.EnterAltScreen)
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -106,7 +113,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
case animTickMsg:
|
||||
m.animationFrame++
|
||||
|
||||
if m.transition == transitionGlitch {
|
||||
m.transitionTick++
|
||||
if m.transitionTick > 5 {
|
||||
m.transition = transitionScan
|
||||
m.transitionTick = 0
|
||||
}
|
||||
} else if m.transition == transitionScan {
|
||||
m.transitionTick++
|
||||
if m.transitionTick > 8 {
|
||||
m.transition = transitionTypewriter
|
||||
m.transitionTick = 0
|
||||
m.typewriterBuf = m.renderContent()
|
||||
m.typewriterPos = 0
|
||||
}
|
||||
} else if m.transition == transitionTypewriter {
|
||||
m.typewriterPos += 3
|
||||
if m.typewriterPos >= len(m.typewriterBuf) {
|
||||
m.transition = transitionNone
|
||||
}
|
||||
}
|
||||
|
||||
return m, animTick()
|
||||
case clockTickMsg:
|
||||
m.currentTime = msg.time
|
||||
return m, clockTick()
|
||||
case progress.FrameMsg:
|
||||
pm, cmd := m.progressBar.Update(msg)
|
||||
m.progressBar = pm.(progress.Model)
|
||||
@@ -120,7 +152,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
case termExitMsg:
|
||||
m.termRunning = false
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render("(process exited)"))
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render("(process exited)"))
|
||||
m.termCmd = nil
|
||||
if m.activeTab == tabShell {
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
@@ -152,7 +184,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case aiErrMsg:
|
||||
m.chatLoading = false
|
||||
m.termAILoading = false
|
||||
errText := errMsgStyle.Render(" error: " + msg.err.Error())
|
||||
errText := errMsgStyle.Render(" [ERROR] " + msg.err.Error())
|
||||
if m.activeTab == tabShell && m.termAIShow {
|
||||
m.termAIChat = append(m.termAIChat, errText)
|
||||
} else {
|
||||
@@ -168,9 +200,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case installCompleteMsg:
|
||||
m.installing = false
|
||||
for _, r := range msg.results {
|
||||
status := itemOKStyle.Render("✓")
|
||||
status := itemOKStyle.Render("[OK]")
|
||||
if !r.Success {
|
||||
status = itemMissingStyle.Render("✗")
|
||||
status = itemMissingStyle.Render("[--]")
|
||||
}
|
||||
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message))
|
||||
}
|
||||
@@ -179,7 +211,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
return m, nil
|
||||
case installProgressMsg:
|
||||
status := itemOKStyle.Render("✓")
|
||||
status := itemOKStyle.Render("[OK]")
|
||||
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool))
|
||||
m.installCurrent = msg.current
|
||||
m.installTool = ""
|
||||
@@ -188,9 +220,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
return m, nil
|
||||
case installBatchMsg:
|
||||
status := itemOKStyle.Render("✓")
|
||||
status := itemOKStyle.Render("[OK]")
|
||||
if !msg.result.Success {
|
||||
status = itemMissingStyle.Render("✗")
|
||||
status = itemMissingStyle.Render("[--]")
|
||||
}
|
||||
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message))
|
||||
m.installCurrent = msg.index + 1
|
||||
@@ -254,7 +286,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m Model) View() string {
|
||||
if !m.ready {
|
||||
return lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Loading muyue...")
|
||||
return lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("Initializing muyue...")
|
||||
}
|
||||
|
||||
if m.showingQuit {
|
||||
@@ -265,6 +297,32 @@ func (m Model) View() string {
|
||||
return m.renderTabMenuOverlay()
|
||||
}
|
||||
|
||||
if m.transition == transitionGlitch {
|
||||
return renderGlitchEffect(m.width, m.height, m.transitionTick)
|
||||
}
|
||||
|
||||
if m.transition == transitionScan {
|
||||
return renderScanEffect(m.width, m.height, m.transitionTick)
|
||||
}
|
||||
|
||||
if m.transition == transitionTypewriter {
|
||||
var b strings.Builder
|
||||
b.WriteString(m.renderHeader())
|
||||
b.WriteString("\n")
|
||||
b.WriteString(typewriterRender(m.typewriterBuf, m.typewriterPos))
|
||||
if m.activeTab == tabStudio {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.renderStudioInput())
|
||||
}
|
||||
if m.activeTab == tabShell {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.renderShellInput())
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.renderFooter())
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(m.renderHeader())
|
||||
b.WriteString("\n")
|
||||
@@ -283,45 +341,6 @@ func (m Model) View() string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderHeader() string {
|
||||
var tabs []string
|
||||
for i, name := range tabNames {
|
||||
icon := tabIcons[i]
|
||||
if tab(i) == m.activeTab {
|
||||
tabStyle := lipgloss.NewStyle().
|
||||
Background(primaryColor).
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
Padding(0, 2)
|
||||
tabs = append(tabs, tabStyle.Render(icon+" "+name))
|
||||
} else {
|
||||
tabStyle := lipgloss.NewStyle().
|
||||
Background(bgPanel).
|
||||
Foreground(textDimColor).
|
||||
Padding(0, 2)
|
||||
tabs = append(tabs, tabStyle.Render(icon+" "+name))
|
||||
}
|
||||
}
|
||||
|
||||
tabLine := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...))
|
||||
|
||||
badge := lipgloss.NewStyle().
|
||||
Foreground(roseColor).
|
||||
Bold(true).
|
||||
Render("muyue")
|
||||
versionBadge := lipgloss.NewStyle().
|
||||
Foreground(dimColor).
|
||||
Render("v" + version.Version)
|
||||
|
||||
anim := lipgloss.NewStyle().Foreground(warmColor).Render(getAnimFrame(m.animationFrame))
|
||||
|
||||
logoLine := lipgloss.NewStyle().Background(bgDark).Padding(0, 1).Render(
|
||||
lipgloss.JoinHorizontal(lipgloss.Center, badge, " ", versionBadge, " ", anim),
|
||||
)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, logoLine, tabLine)
|
||||
}
|
||||
|
||||
func (m Model) renderContent() string {
|
||||
switch m.activeTab {
|
||||
case tabDashboard:
|
||||
@@ -354,127 +373,6 @@ func (m *Model) resizeViewport() {
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
}
|
||||
|
||||
func (m Model) renderFooter() string {
|
||||
profile := "unknown"
|
||||
if m.config != nil && m.config.Profile.Pseudo != "" {
|
||||
profile = m.config.Profile.Pseudo
|
||||
}
|
||||
|
||||
left := fmt.Sprintf(" %s@%s",
|
||||
lipgloss.NewStyle().Foreground(roseColor).Bold(true).Render(profile),
|
||||
lipgloss.NewStyle().Foreground(dimColor).Render(version.Name))
|
||||
leftR := statusBarStyle.Render(left)
|
||||
|
||||
var helpText string
|
||||
switch m.activeTab {
|
||||
case tabDashboard:
|
||||
helpText = "[i] install [u] update [s] scan [ctrl+t] tabs"
|
||||
case tabStudio:
|
||||
helpText = "[enter] send [ctrl+s] sidebar [ctrl+t] tabs"
|
||||
case tabShell:
|
||||
helpText = "[enter] run [ctrl+a] AI panel [ctrl+c] kill"
|
||||
case tabConfig:
|
||||
helpText = "[↑↓] sections [ctrl+t] tabs"
|
||||
default:
|
||||
helpText = "[ctrl+t] tabs [ctrl+c] quit"
|
||||
}
|
||||
rightR := statusBarStyle.Render(helpText)
|
||||
|
||||
gap := m.width - lipgloss.Width(leftR) - lipgloss.Width(rightR)
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
|
||||
statusLine := lipgloss.JoinHorizontal(lipgloss.Bottom,
|
||||
leftR,
|
||||
strings.Repeat(" ", gap),
|
||||
rightR,
|
||||
)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, statusLine,
|
||||
lipgloss.NewStyle().Background(bgPanel).Foreground(dimColor).Render(
|
||||
lipgloss.NewStyle().Padding(0, 1).Render(m.helpModel.View(keys))))
|
||||
}
|
||||
|
||||
func (m Model) renderQuitOverlay() string {
|
||||
yesStyle := confirmNoStyle
|
||||
noStyle := confirmYesStyle
|
||||
if m.confirmCursor == 0 {
|
||||
yesStyle = confirmYesStyle
|
||||
noStyle = confirmNoStyle
|
||||
}
|
||||
|
||||
frame := lipgloss.NewStyle().Foreground(primaryColor).Render(getAnimFrame(m.animationFrame))
|
||||
|
||||
box := fmt.Sprintf("\n\n %s Quit muyue?\n\n %s %s",
|
||||
frame,
|
||||
yesStyle.Render("[ Yes ]"),
|
||||
noStyle.Render("[ No ]"),
|
||||
)
|
||||
|
||||
content := confirmBoxStyle.Render(box)
|
||||
|
||||
return lipgloss.Place(m.width, m.height,
|
||||
0.5, 0.5,
|
||||
content,
|
||||
lipgloss.WithWhitespaceBackground(bgDark),
|
||||
lipgloss.WithWhitespaceForeground(dimColor),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) renderTabMenuOverlay() string {
|
||||
menuStyle := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(primaryColor).
|
||||
Background(bgCard).
|
||||
Padding(1, 3)
|
||||
|
||||
tabItemStyle := lipgloss.NewStyle().
|
||||
Foreground(textDimColor).
|
||||
Padding(0, 2)
|
||||
|
||||
tabItemActiveStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Background(primaryColor).
|
||||
Bold(true).
|
||||
Padding(0, 2)
|
||||
|
||||
descs := []string{
|
||||
"tools, updates & system status",
|
||||
"chat, agents & workflows",
|
||||
"terminal + AI assistant",
|
||||
"profile, API keys & settings",
|
||||
}
|
||||
|
||||
var items []string
|
||||
for i, name := range tabNames {
|
||||
num := lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %d.", i+1))
|
||||
icon := tabIcons[i] + " "
|
||||
if i == m.tabMenuCursor {
|
||||
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(roseLightColor).Render(descs[i]))
|
||||
items = append(items, tabItemActiveStyle.Render("▸"+item))
|
||||
} else {
|
||||
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(dimColor).Render(descs[i]))
|
||||
items = append(items, tabItemStyle.Render(" "+item))
|
||||
}
|
||||
}
|
||||
|
||||
header := lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Switch Tab")
|
||||
content := header + "\n\n" +
|
||||
strings.Join(items, "\n") +
|
||||
"\n\n" +
|
||||
lipgloss.NewStyle().Foreground(dimColor).Render("↑↓ navigate · enter select · esc cancel")
|
||||
|
||||
box := menuStyle.Render(content)
|
||||
|
||||
return lipgloss.Place(m.width, m.height,
|
||||
0.5, 0.5,
|
||||
box,
|
||||
lipgloss.WithWhitespaceBackground(bgDark),
|
||||
lipgloss.WithWhitespaceForeground(dimColor),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *Model) handlePreview(files []previewFile) {
|
||||
dir := filepath.Join(os.TempDir(), "muyue-preview")
|
||||
os.RemoveAll(dir)
|
||||
@@ -495,22 +393,3 @@ func (m *Model) handlePreview(files []previewFile) {
|
||||
m.chatLog = append(m.chatLog, itemOKStyle.Render(" Preview: http://127.0.0.1:8765"))
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) renderStudioInput() string {
|
||||
if m.chatLoading {
|
||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
||||
inputStyle.Render("⟩ ") + m.spinner.View() + lipgloss.NewStyle().Foreground(mutedColor).Render(" thinking..."),
|
||||
)
|
||||
}
|
||||
cursor := lipgloss.NewStyle().Foreground(primaryColor).Render("▎")
|
||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
||||
inputStyle.Render("⟩ ") + m.chatInput + cursor,
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) renderShellInput() string {
|
||||
prompt := lipgloss.NewStyle().Foreground(successColor).Render("❯ ")
|
||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
||||
prompt + m.termInput + lipgloss.NewStyle().Foreground(primaryColor).Render("▎"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (m Model) renderStudio() string {
|
||||
func (m Model) renderStudioSidebar(width int) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(renderSectionWithIcon("Studio", "◈"))
|
||||
b.WriteString(renderSectionHeader("STUDIO", "[<>]"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
panels := []struct {
|
||||
@@ -38,23 +38,23 @@ func (m Model) renderStudioSidebar(width int) string {
|
||||
panel studioPanel
|
||||
icon string
|
||||
}{
|
||||
{"Chat", panelChat, "💬"},
|
||||
{"Agents", panelAgents, "◉"},
|
||||
{"Workflows", panelWorkflows, "⟐"},
|
||||
{"Chat", panelChat, "[#]"},
|
||||
{"Agents", panelAgents, "[*]"},
|
||||
{"Workflows", panelWorkflows, "[~]"},
|
||||
}
|
||||
|
||||
for _, p := range panels {
|
||||
if m.studioPanel == p.panel {
|
||||
activeStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Background(primaryColor).
|
||||
Background(cyberRed).
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
b.WriteString(activeStyle.Render(p.icon + " " + p.name))
|
||||
b.WriteString("\n")
|
||||
} else {
|
||||
inactiveStyle := lipgloss.NewStyle().
|
||||
Foreground(textDimColor).
|
||||
Foreground(textDim).
|
||||
Padding(0, 1)
|
||||
b.WriteString(inactiveStyle.Render(p.icon + " " + p.name))
|
||||
b.WriteString("\n")
|
||||
@@ -62,7 +62,7 @@ func (m Model) renderStudioSidebar(width int) string {
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", width-4)))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", width-4)))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
switch m.studioPanel {
|
||||
@@ -78,26 +78,26 @@ func (m Model) renderStudioSidebar(width int) string {
|
||||
}
|
||||
|
||||
func (m Model) renderChatSidebar(b *strings.Builder, width int) {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Active Provider"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Active Provider"))
|
||||
b.WriteString("\n")
|
||||
provider := "none"
|
||||
if m.config != nil {
|
||||
provider = m.config.Profile.Preferences.DefaultAI
|
||||
}
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(roseColor).Bold(true).Render(" " + provider))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" " + provider))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Commands"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Commands"))
|
||||
b.WriteString("\n")
|
||||
cmds := []string{"/plan <goal>", "/help"}
|
||||
for _, c := range cmds {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(" " + c))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(" " + c))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if m.previewURL != "" {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Preview"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Preview"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(itemOKStyle.Render(" " + m.previewURL))
|
||||
b.WriteString("\n")
|
||||
@@ -121,43 +121,43 @@ func (m Model) renderAgentsSidebar(b *strings.Builder, width int) {
|
||||
var statusIcon string
|
||||
switch status {
|
||||
case proxy.StatusRunning:
|
||||
statusIcon = lipgloss.NewStyle().Foreground(warmColor).Render("● running")
|
||||
statusIcon = lipgloss.NewStyle().Foreground(neonRed).Render("[>> running]")
|
||||
case proxy.StatusStopped:
|
||||
statusIcon = lipgloss.NewStyle().Foreground(mutedColor).Render("○ stopped")
|
||||
statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[|| stopped]")
|
||||
case proxy.StatusError:
|
||||
statusIcon = lipgloss.NewStyle().Foreground(errorColor).Render("✗ error")
|
||||
statusIcon = lipgloss.NewStyle().Foreground(errorRed).Render("[!! error]")
|
||||
default:
|
||||
if available {
|
||||
statusIcon = lipgloss.NewStyle().Foreground(successColor).Render("✓ available")
|
||||
statusIcon = lipgloss.NewStyle().Foreground(successGreen).Render("[OK available]")
|
||||
} else {
|
||||
statusIcon = lipgloss.NewStyle().Foreground(dimColor).Render("✗ not installed")
|
||||
statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[-- not installed]")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Bold(true).Render(a.name))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textBright).Bold(true).Render(a.name))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" %s\n", statusIcon))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s\n", a.tool)))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s\n", a.tool)))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Actions"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Actions"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [c]"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Crush"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [c]"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Crush"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [l]"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Claude"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [l]"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Claude"))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
||||
if m.orch == nil || m.orch.Workflow == nil {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("No active workflow."))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("No active workflow."))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("Use /plan <goal> in chat"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("Use /plan <goal> in chat"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("to start a workflow."))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("to start a workflow."))
|
||||
b.WriteString("\n")
|
||||
return
|
||||
}
|
||||
@@ -165,13 +165,13 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
||||
wf := m.orch.Workflow
|
||||
|
||||
phaseColors := map[workflow.Phase]lipgloss.Style{
|
||||
workflow.PhaseIdle: lipgloss.NewStyle().Foreground(mutedColor),
|
||||
workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warningColor).Bold(true),
|
||||
workflow.PhasePlanning: lipgloss.NewStyle().Foreground(roseColor).Bold(true),
|
||||
workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(accentColor).Bold(true),
|
||||
workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(primaryColor).Bold(true),
|
||||
workflow.PhaseDone: lipgloss.NewStyle().Foreground(successColor).Bold(true),
|
||||
workflow.PhaseError: lipgloss.NewStyle().Foreground(errorColor).Bold(true),
|
||||
workflow.PhaseIdle: lipgloss.NewStyle().Foreground(textMuted),
|
||||
workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warnAmber).Bold(true),
|
||||
workflow.PhasePlanning: lipgloss.NewStyle().Foreground(cyberPink).Bold(true),
|
||||
workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(cyberRose).Bold(true),
|
||||
workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(cyberRed).Bold(true),
|
||||
workflow.PhaseDone: lipgloss.NewStyle().Foreground(successGreen).Bold(true),
|
||||
workflow.PhaseError: lipgloss.NewStyle().Foreground(errorRed).Bold(true),
|
||||
}
|
||||
|
||||
if style, ok := phaseColors[wf.Phase]; ok {
|
||||
@@ -180,9 +180,9 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if wf.Plan.Goal != "" {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Goal"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Goal"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(wf.Plan.Goal))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(wf.Plan.Goal))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Controls"))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Controls"))
|
||||
b.WriteString("\n")
|
||||
controls := []struct {
|
||||
key string
|
||||
@@ -207,8 +207,8 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
||||
{"[x]", "Cancel"},
|
||||
}
|
||||
for _, c := range controls {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" " + c.key))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textDimColor).Render(" " + c.desc))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" "+c.key))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(textDim).Render(" "+c.desc))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
@@ -216,14 +216,14 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
|
||||
func (m Model) renderStudioChat(width int) string {
|
||||
var b strings.Builder
|
||||
|
||||
chatHeader := renderSectionWithIcon("Chat", "💬")
|
||||
chatHeader := renderSectionHeader("CHAT", "[#]")
|
||||
if m.chatLoading {
|
||||
chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warningColor).Render("thinking...")
|
||||
chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warnAmber).Render("thinking...")
|
||||
}
|
||||
b.WriteString(chatHeader)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10)))
|
||||
sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
|
||||
b.WriteString(" " + sep)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
@@ -235,6 +235,18 @@ func (m Model) renderStudioChat(width int) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderStudioInput() string {
|
||||
if m.chatLoading {
|
||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
||||
inputStyle.Render(">> ") + m.spinner.View() + lipgloss.NewStyle().Foreground(textMuted).Render(" thinking..."),
|
||||
)
|
||||
}
|
||||
cursor := lipgloss.NewStyle().Foreground(cyberRed).Render("▎")
|
||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
||||
inputStyle.Render(">> ") + m.chatInput + cursor,
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) handleStudioPanelSwitch(panel studioPanel) {
|
||||
m.studioPanel = panel
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
|
||||
@@ -5,127 +5,192 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
primaryColor = lipgloss.Color("#E8364F")
|
||||
roseColor = lipgloss.Color("#FF6B8A")
|
||||
roseLightColor = lipgloss.Color("#FFB3C6")
|
||||
accentColor = lipgloss.Color("#FF8FA3")
|
||||
warmColor = lipgloss.Color("#FF4D6D")
|
||||
successColor = lipgloss.Color("#4ADE80")
|
||||
warningColor = lipgloss.Color("#FBBF24")
|
||||
errorColor = lipgloss.Color("#FF4D4D")
|
||||
mutedColor = lipgloss.Color("#8B7E8E")
|
||||
dimColor = lipgloss.Color("#5A4F5E")
|
||||
textColor = lipgloss.Color("#F0E6E8")
|
||||
textDimColor = lipgloss.Color("#B8A9AD")
|
||||
cyberRed = lipgloss.Color("#FF0033")
|
||||
cyberRedDark = lipgloss.Color("#8B0020")
|
||||
cyberRedDeep = lipgloss.Color("#5C0015")
|
||||
cyberPink = lipgloss.Color("#FF1A5E")
|
||||
cyberRose = lipgloss.Color("#FF4D6D")
|
||||
neonRed = lipgloss.Color("#FF1744")
|
||||
brightRed = lipgloss.Color("#FF5252")
|
||||
dimRed = lipgloss.Color("#6B2033")
|
||||
mutedRed = lipgloss.Color("#4A1525")
|
||||
|
||||
bgDark = lipgloss.Color("#0D0A0B")
|
||||
bgPanel = lipgloss.Color("#1A1215")
|
||||
bgCard = lipgloss.Color("#231A1D")
|
||||
bgInput = lipgloss.Color("#2A2023")
|
||||
bgHover = lipgloss.Color("#332528")
|
||||
textBright = lipgloss.Color("#EAE0E2")
|
||||
textMain = lipgloss.Color("#D4C4C8")
|
||||
textDim = lipgloss.Color("#8A7A7E")
|
||||
textMuted = lipgloss.Color("#5A4F52")
|
||||
|
||||
borderColor = lipgloss.Color("#3D2E32")
|
||||
borderAccent = lipgloss.Color("#E8364F")
|
||||
successGreen = lipgloss.Color("#00E676")
|
||||
warnAmber = lipgloss.Color("#FFD740")
|
||||
errorRed = lipgloss.Color("#FF1744")
|
||||
|
||||
tabActiveBg = lipgloss.Color("#E8364F")
|
||||
tabInactiveBg = lipgloss.Color("#1A1215")
|
||||
bgVoid = lipgloss.Color("#0A0A0C")
|
||||
bgBase = lipgloss.Color("#0F0D10")
|
||||
bgSurface = lipgloss.Color("#161218")
|
||||
bgPanel = lipgloss.Color("#1C1719")
|
||||
bgCard = lipgloss.Color("#221B1E")
|
||||
bgInput = lipgloss.Color("#2A2225")
|
||||
|
||||
sectionStyle = lipgloss.NewStyle().
|
||||
Foreground(roseColor).
|
||||
borderDim = lipgloss.Color("#2A1F22")
|
||||
borderRed = lipgloss.Color("#FF003344")
|
||||
borderRedFull = lipgloss.Color("#FF0033")
|
||||
)
|
||||
|
||||
var (
|
||||
baseStyle = lipgloss.NewStyle()
|
||||
|
||||
titleBlockStyle = lipgloss.NewStyle().
|
||||
Foreground(cyberRed).
|
||||
Bold(true)
|
||||
|
||||
sectionIconStyle = lipgloss.NewStyle().
|
||||
Foreground(primaryColor).
|
||||
Bold(true)
|
||||
|
||||
itemOKStyle = lipgloss.NewStyle().
|
||||
Foreground(successColor)
|
||||
|
||||
itemMissingStyle = lipgloss.NewStyle().
|
||||
Foreground(errorColor)
|
||||
|
||||
itemWarnStyle = lipgloss.NewStyle().
|
||||
Foreground(warningColor)
|
||||
|
||||
itemPendingStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor)
|
||||
|
||||
userMsgStyle = lipgloss.NewStyle().
|
||||
Foreground(roseLightColor)
|
||||
|
||||
aiMsgStyle = lipgloss.NewStyle().
|
||||
Foreground(textColor)
|
||||
|
||||
errMsgStyle = lipgloss.NewStyle().
|
||||
Foreground(errorColor)
|
||||
|
||||
inputStyle = lipgloss.NewStyle().
|
||||
Foreground(roseColor)
|
||||
|
||||
stepDoneStyle = lipgloss.NewStyle().
|
||||
Foreground(successColor)
|
||||
|
||||
stepPendingStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor)
|
||||
|
||||
stepCurrentStyle = lipgloss.NewStyle().
|
||||
Foreground(primaryColor).
|
||||
Bold(true)
|
||||
|
||||
stepErrorStyle = lipgloss.NewStyle().
|
||||
Foreground(errorColor)
|
||||
|
||||
statusBarStyle = lipgloss.NewStyle().
|
||||
Background(bgPanel).
|
||||
Foreground(textDimColor).
|
||||
Padding(0, 1)
|
||||
|
||||
confirmBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(primaryColor).
|
||||
Background(bgCard).
|
||||
Foreground(textColor).
|
||||
Padding(1, 3).
|
||||
sectionTitleStyle = lipgloss.NewStyle().
|
||||
Foreground(cyberRed).
|
||||
Bold(true)
|
||||
|
||||
confirmYesStyle = lipgloss.NewStyle().
|
||||
Foreground(successColor).
|
||||
Bold(true)
|
||||
labelStyle = lipgloss.NewStyle().
|
||||
Foreground(textDim).
|
||||
Width(14)
|
||||
|
||||
confirmNoStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor)
|
||||
valueStyle = lipgloss.NewStyle().
|
||||
Foreground(textMain)
|
||||
|
||||
cardStyle = lipgloss.NewStyle().
|
||||
Background(bgCard).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(borderColor).
|
||||
BorderForeground(borderDim).
|
||||
Padding(0, 1)
|
||||
|
||||
cardActiveStyle = lipgloss.NewStyle().
|
||||
Background(bgCard).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(cyberRed).
|
||||
Padding(0, 1)
|
||||
|
||||
sidebarStyle = lipgloss.NewStyle().
|
||||
Background(bgPanel).
|
||||
Background(bgSurface).
|
||||
Border(lipgloss.Border{Right: "│"}).
|
||||
BorderForeground(borderColor).
|
||||
BorderForeground(borderDim).
|
||||
Padding(0, 1)
|
||||
|
||||
statusBarStyle = lipgloss.NewStyle().
|
||||
Background(bgSurface).
|
||||
Foreground(textDim).
|
||||
Padding(0, 1)
|
||||
|
||||
inputStyle = lipgloss.NewStyle().
|
||||
Foreground(cyberRed)
|
||||
|
||||
userMsgStyle = lipgloss.NewStyle().
|
||||
Foreground(cyberRose)
|
||||
|
||||
aiMsgStyle = lipgloss.NewStyle().
|
||||
Foreground(textMain)
|
||||
|
||||
errMsgStyle = lipgloss.NewStyle().
|
||||
Foreground(errorRed)
|
||||
|
||||
itemOKStyle = lipgloss.NewStyle().Foreground(successGreen)
|
||||
itemMissingStyle = lipgloss.NewStyle().Foreground(errorRed)
|
||||
itemWarnStyle = lipgloss.NewStyle().Foreground(warnAmber)
|
||||
itemPendingStyle = lipgloss.NewStyle().Foreground(textMuted)
|
||||
|
||||
confirmBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(cyberRed).
|
||||
Background(bgCard).
|
||||
Foreground(textBright).
|
||||
Padding(1, 3).
|
||||
Bold(true)
|
||||
|
||||
confirmYesStyle = lipgloss.NewStyle().Foreground(successGreen).Bold(true)
|
||||
confirmNoStyle = lipgloss.NewStyle().Foreground(textMuted)
|
||||
|
||||
badgeStyle = lipgloss.NewStyle().
|
||||
Background(primaryColor).
|
||||
Background(cyberRed).
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Padding(0, 1).
|
||||
Bold(true)
|
||||
|
||||
labelStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor).
|
||||
Width(14)
|
||||
tabBarStyle = lipgloss.NewStyle().Background(bgSurface)
|
||||
|
||||
valueStyle = lipgloss.NewStyle().
|
||||
Foreground(textColor)
|
||||
|
||||
tabBarStyle = lipgloss.NewStyle().
|
||||
Background(bgPanel)
|
||||
|
||||
pulseFrames = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}
|
||||
stepDoneStyle = lipgloss.NewStyle().Foreground(successGreen)
|
||||
stepPendingStyle = lipgloss.NewStyle().Foreground(textMuted)
|
||||
stepCurrentStyle = lipgloss.NewStyle().Foreground(cyberRed).Bold(true)
|
||||
stepErrorStyle = lipgloss.NewStyle().Foreground(errorRed)
|
||||
)
|
||||
|
||||
func getAnimFrame(frame int) string {
|
||||
return pulseFrames[frame%len(pulseFrames)]
|
||||
var logoLines = []string{
|
||||
"███╗ ███╗██╗ ██╗ █████╗ ███╗ ██╗███████╗",
|
||||
"████╗ ████║╚██╗ ██╔╝██╔══██╗████╗ ██║██╔════╝",
|
||||
"██╔████╔██║ ╚████╔╝ ███████║██╔██╗ ██║███████╗",
|
||||
"██║╚██╔╝██║ ╚██╔╝ ██╔══██║██║╚██╗██║╚════██║",
|
||||
"██║ ╚═╝ ██║ ██║ ██║ ██║██║ ╚████║███████║",
|
||||
"╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
|
||||
}
|
||||
|
||||
var scanFrames = []string{
|
||||
"─────────────────────────── ───",
|
||||
" ─────────────────────────── ─── ",
|
||||
"── ──────────────────────────── ",
|
||||
"─ ─── ────────────────────────────",
|
||||
"─── ─────────────────────────── ─",
|
||||
" ──── ─────────────────────────────",
|
||||
}
|
||||
|
||||
func getAnimFrame(frame int) string {
|
||||
frames := []string{
|
||||
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
|
||||
}
|
||||
return frames[frame%len(frames)]
|
||||
}
|
||||
|
||||
func getScanFrame(frame int) string {
|
||||
return scanFrames[frame%len(scanFrames)]
|
||||
}
|
||||
|
||||
func renderLogo() string {
|
||||
styled := make([]string, len(logoLines))
|
||||
for i, line := range logoLines {
|
||||
styled[i] = lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(line)
|
||||
}
|
||||
return lipgloss.JoinVertical(lipgloss.Left, styled...)
|
||||
}
|
||||
|
||||
func renderBlockTitle(text string) string {
|
||||
width := len(text) + 6
|
||||
top := lipgloss.NewStyle().Foreground(dimRed).Render(
|
||||
"╭" + repeatStr("─", width) + "╮",
|
||||
)
|
||||
content := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(
|
||||
"│ ■ " + text + " ■ │",
|
||||
)
|
||||
bottom := lipgloss.NewStyle().Foreground(dimRed).Render(
|
||||
"╰" + repeatStr("─", width) + "╯",
|
||||
)
|
||||
return lipgloss.JoinVertical(lipgloss.Left, top, content, bottom)
|
||||
}
|
||||
|
||||
func renderSectionHeader(title string, icon string) string {
|
||||
return lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(
|
||||
"■ "+icon+" "+title+" ■",
|
||||
)
|
||||
}
|
||||
|
||||
func renderProgressBar(pct float64, width int) string {
|
||||
filled := int(float64(width) * pct)
|
||||
empty := width - filled
|
||||
bar := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(
|
||||
repeatStr("█", filled),
|
||||
) + lipgloss.NewStyle().Foreground(dimRed).Render(
|
||||
repeatStr("░", empty),
|
||||
)
|
||||
return bar
|
||||
}
|
||||
|
||||
func repeatStr(s string, n int) string {
|
||||
result := ""
|
||||
for i := 0; i < n; i++ {
|
||||
result += s
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@ func (m Model) renderShell() string {
|
||||
func (m Model) renderTermPanel(width int) string {
|
||||
var b strings.Builder
|
||||
|
||||
cwdStyle := lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd)
|
||||
b.WriteString(renderSectionWithIcon("Terminal", "▶"))
|
||||
cwdStyle := lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd)
|
||||
b.WriteString(renderSectionHeader("TERMINAL", "[$]"))
|
||||
b.WriteString(" ")
|
||||
b.WriteString(cwdStyle)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10)))
|
||||
sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
|
||||
b.WriteString(" " + sep)
|
||||
b.WriteString("\n")
|
||||
|
||||
@@ -74,10 +74,10 @@ func (m Model) renderTermPanel(width int) string {
|
||||
func (m Model) renderAIPanel(width int) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(renderSectionWithIcon("AI Assistant", "◈"))
|
||||
b.WriteString(renderSectionHeader("AI ASSISTANT", "[?]"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10)))
|
||||
sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
|
||||
b.WriteString(" " + sep)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
@@ -87,30 +87,37 @@ func (m Model) renderAIPanel(width int) string {
|
||||
}
|
||||
|
||||
if m.termAILoading {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(warmColor).Render(" " + getAnimFrame(m.animationFrame) + " thinking..."))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(neonRed).Render(" " + getAnimFrame(m.animationFrame) + " thinking..."))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
inputLabel := lipgloss.NewStyle().Foreground(roseColor).Render("⟩ ")
|
||||
inputLabel := lipgloss.NewStyle().Foreground(cyberRed).Render(">> ")
|
||||
b.WriteString(inputLabel)
|
||||
b.WriteString(m.termAIInput)
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Background(bgPanel).
|
||||
Background(bgSurface).
|
||||
Border(lipgloss.Border{Left: "│"}).
|
||||
BorderForeground(borderColor).
|
||||
BorderForeground(borderDim).
|
||||
Width(width).
|
||||
Padding(0, 1).
|
||||
Render(b.String())
|
||||
}
|
||||
|
||||
func (m Model) renderShellInput() string {
|
||||
prompt := lipgloss.NewStyle().Foreground(successGreen).Render("> ")
|
||||
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
|
||||
prompt + m.termInput + lipgloss.NewStyle().Foreground(cyberRed).Render("▎"),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
if m.termCmd != nil && m.termCmd.Process != nil {
|
||||
m.termCmd.Process.Kill()
|
||||
m.termRunning = false
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorColor).Render("^C"))
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorRed).Render("^C"))
|
||||
m.termCmd = nil
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
return m, nil
|
||||
@@ -151,7 +158,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
if isDangerousCommand(input) {
|
||||
m.termLog = append(m.termLog, errMsgStyle.Render(" blocked: potentially dangerous command"))
|
||||
m.termLog = append(m.termLog, errMsgStyle.Render(" [BLOCKED] potentially dangerous command"))
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
m.viewport.GotoBottom()
|
||||
return m, nil
|
||||
@@ -165,7 +172,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
if err := os.Chdir(dir); err == nil {
|
||||
m.termCwd, _ = os.Getwd()
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ "+input))
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input)
|
||||
} else {
|
||||
m.termLog = append(m.termLog, errMsgStyle.Render(" cd: "+err.Error()))
|
||||
}
|
||||
@@ -173,7 +180,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.viewport.GotoBottom()
|
||||
return m, nil
|
||||
}
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ ")+input)
|
||||
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input)
|
||||
m.viewport.SetContent(m.renderContent())
|
||||
m.viewport.GotoBottom()
|
||||
return m, m.runTermCommand(input)
|
||||
|
||||
@@ -32,8 +32,8 @@ const (
|
||||
tabCount
|
||||
)
|
||||
|
||||
var tabNames = []string{"Dashboard", "Studio", "Shell", "Config"}
|
||||
var tabIcons = []string{"◉", "◈", "▶", "⚙"}
|
||||
var tabNames = []string{"DASH", "STUDIO", "SHELL", "CONFIG"}
|
||||
var tabIcons = []string{"[■]", "[<>]", "[>$]", "[//]"}
|
||||
|
||||
type aiResponseMsg struct{ content string }
|
||||
type aiErrMsg struct{ err error }
|
||||
@@ -61,6 +61,9 @@ type spinnerTickMsg struct{ time time.Time }
|
||||
type termOutputMsg struct{ line string }
|
||||
type termExitMsg struct{}
|
||||
type animTickMsg struct{ time time.Time }
|
||||
type clockTickMsg struct{ time time.Time }
|
||||
type glitchDoneMsg struct{}
|
||||
type scanDoneMsg struct{}
|
||||
|
||||
type studioPanel int
|
||||
|
||||
@@ -79,10 +82,20 @@ const (
|
||||
configSkills
|
||||
)
|
||||
|
||||
type transitionState int
|
||||
|
||||
const (
|
||||
transitionNone transitionState = iota
|
||||
transitionGlitch
|
||||
transitionScan
|
||||
transitionTypewriter
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
config *config.MuyueConfig
|
||||
scanResult *scanner.ScanResult
|
||||
activeTab tab
|
||||
prevTab tab
|
||||
width int
|
||||
height int
|
||||
viewport viewport.Model
|
||||
@@ -126,7 +139,7 @@ type Model struct {
|
||||
termRunning bool
|
||||
termCwd string
|
||||
|
||||
studioPanel studioPanel
|
||||
studioPanel studioPanel
|
||||
studioSidebarOpen bool
|
||||
|
||||
termAIChat []string
|
||||
@@ -138,6 +151,12 @@ type Model struct {
|
||||
configField int
|
||||
|
||||
animationFrame int
|
||||
|
||||
transition transitionState
|
||||
transitionTick int
|
||||
typewriterBuf string
|
||||
typewriterPos int
|
||||
currentTime time.Time
|
||||
}
|
||||
|
||||
type keyMap struct {
|
||||
|
||||
@@ -2,7 +2,7 @@ package version
|
||||
|
||||
const (
|
||||
Name = "muyue"
|
||||
Version = "0.2.0"
|
||||
Version = "0.2.1"
|
||||
Author = "La Légion de Muyue"
|
||||
License = "MIT"
|
||||
)
|
||||
|
||||
4
web/.gitignore
vendored
Normal file
4
web/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
!dist/.gitkeep
|
||||
.vite/
|
||||
6
web/embed.go
Normal file
6
web/embed.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed all:dist
|
||||
var Assets embed.FS
|
||||
13
web/index.html
Normal file
13
web/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>muyue</title>
|
||||
<link rel="stylesheet" href="/src/styles/global.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
965
web/package-lock.json
generated
Normal file
965
web/package-lock.json
generated
Normal file
@@ -0,0 +1,965 @@
|
||||
{
|
||||
"name": "muyue-web",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "muyue-web",
|
||||
"dependencies": {
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"vite": "^8.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
||||
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
||||
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-rc.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rolldown/plugin-babel": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-react-compiler": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
|
||||
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.126.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.16"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
|
||||
"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.10",
|
||||
"rolldown": "1.0.0-rc.16",
|
||||
"tinyglobby": "^0.2.16"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
web/package.json
Normal file
18
web/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "muyue-web",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"vite": "^8.0.9"
|
||||
}
|
||||
}
|
||||
31
web/src/api/client.js
Normal file
31
web/src/api/client.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const API_BASE = '/api'
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(err.error || res.statusText)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
const api = {
|
||||
getInfo: () => request('/info'),
|
||||
getSystem: () => request('/system'),
|
||||
getTools: () => request('/tools'),
|
||||
getConfig: () => request('/config'),
|
||||
getProviders: () => request('/providers'),
|
||||
getSkills: () => request('/skills'),
|
||||
getLSP: () => request('/lsp'),
|
||||
getMCP: () => request('/mcp'),
|
||||
getUpdates: () => request('/updates'),
|
||||
runScan: () => request('/scan', { method: 'POST' }),
|
||||
installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }),
|
||||
configureMCP: () => request('/mcp/configure', { method: 'POST' }),
|
||||
runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }),
|
||||
}
|
||||
|
||||
export default api
|
||||
105
web/src/components/App.jsx
Normal file
105
web/src/components/App.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import api from '../api/client'
|
||||
import { getTheme, getThemeNames, applyTheme } from '../themes'
|
||||
import Dashboard from './Dashboard'
|
||||
import Studio from './Studio'
|
||||
import Shell from './Shell'
|
||||
import Config from './Config'
|
||||
|
||||
const TABS = [
|
||||
{ id: 'dash', label: 'DASH', icon: '[■]' },
|
||||
{ id: 'studio', label: 'STUDIO', icon: '[<>]' },
|
||||
{ id: 'shell', label: 'SHELL', icon: '[$]' },
|
||||
{ id: 'config', label: 'CONFIG', icon: '[//]' },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
const [activeTab, setActiveTab] = useState('dash')
|
||||
const [info, setInfo] = useState({})
|
||||
const [clock, setClock] = useState(new Date())
|
||||
const [updates, setUpdates] = useState([])
|
||||
const [tools, setTools] = useState([])
|
||||
const [transition, setTransition] = useState(false)
|
||||
const [currentTheme, setCurrentTheme] = useState('cyberpunk-red')
|
||||
// api is imported directly
|
||||
|
||||
useEffect(() => {
|
||||
api.getInfo().then(setInfo).catch(() => {})
|
||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||
|
||||
const theme = getTheme(currentTheme)
|
||||
applyTheme(theme)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setClock(new Date()), 1000)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
const switchTab = useCallback((tabId) => {
|
||||
if (tabId === activeTab) return
|
||||
setTransition(true)
|
||||
setTimeout(() => {
|
||||
setActiveTab(tabId)
|
||||
setTimeout(() => setTransition(false), 150)
|
||||
}, 100)
|
||||
}, [activeTab])
|
||||
|
||||
const hasUpdates = updates.some(u => u.needsUpdate)
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={(t) => setTools(t)} />
|
||||
case 'studio': return <Studio api={api} />
|
||||
case 'shell': return <Shell api={api} />
|
||||
case 'config': return <Config api={api} theme={currentTheme} onThemeChange={setCurrentTheme} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<header className="header">
|
||||
<span className="header-logo">MUYUE</span>
|
||||
<span className="header-version">v{info.version || '...'}</span>
|
||||
|
||||
<div className="header-tabs">
|
||||
{TABS.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`header-tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => switchTab(tab.id)}
|
||||
>
|
||||
{tab.icon} {tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="header-spacer" />
|
||||
|
||||
<div className="header-status">
|
||||
<span className={`status-dot ${tools.length > 0 ? 'ok' : 'off'}`} title="System" />
|
||||
<span className={`status-dot ${hasUpdates ? 'warn' : 'ok'}`} title="Updates" />
|
||||
</div>
|
||||
|
||||
<span className="header-date">{clock.toLocaleDateString('fr-FR')}</span>
|
||||
<span className="header-clock">{clock.toLocaleTimeString('fr-FR')}</span>
|
||||
</header>
|
||||
|
||||
<div className={`content ${transition ? 'glitch-text' : 'fade-in tab-transition'}`}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
<footer className="footer">
|
||||
<span className="footer-shortcuts">
|
||||
<kbd>1-4</kbd> tabs · <kbd>Ctrl+T</kbd> switcher · <kbd>Ctrl+C</kbd> quit
|
||||
</span>
|
||||
<span className={`footer-update ${hasUpdates ? 'available' : 'uptodate'}`}>
|
||||
{hasUpdates ? '[UPD] Updates available' : '[OK] Up to date'}
|
||||
</span>
|
||||
<span className="footer-version">v{info.version || '...'}</span>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
98
web/src/components/Config.jsx
Normal file
98
web/src/components/Config.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getThemeNames, applyTheme, getTheme } from '../themes'
|
||||
|
||||
export default function Config({ api, theme, onThemeChange }) {
|
||||
const [config, setConfig] = useState(null)
|
||||
const [providers, setProviders] = useState([])
|
||||
const [skillList, setSkillList] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfig().then(d => setConfig(d)).catch(() => {})
|
||||
api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {})
|
||||
api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const themes = getThemeNames()
|
||||
|
||||
const handleThemeChange = (themeId) => {
|
||||
const t = getTheme(themeId)
|
||||
applyTheme(t)
|
||||
onThemeChange(themeId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="config-container">
|
||||
<div className="config-section">
|
||||
<div className="section-header">Profile</div>
|
||||
{config?.profile && (
|
||||
<div>
|
||||
<Field label="Name" value={config.profile.name} />
|
||||
<Field label="Pseudo" value={config.profile.pseudo} />
|
||||
<Field label="Email" value={config.profile.email} />
|
||||
<Field label="Editor" value={config.profile.preferences?.editor} />
|
||||
<Field label="Shell" value={config.profile.preferences?.shell} />
|
||||
<Field label="Default AI" value={config.profile.preferences?.defaultAI} />
|
||||
<Field label="Languages" value={config.profile.languages?.join(', ')} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="section-header">AI Providers</div>
|
||||
{providers.map((p, i) => (
|
||||
<div key={i} className="config-field" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ color: 'var(--text-bright)', fontWeight: 700 }}>{p.name}</span>
|
||||
{p.active && <span style={{ color: 'var(--cyber-red)', fontSize: 11, fontWeight: 700 }}>{'>>'}</span>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16, fontSize: 12 }}>
|
||||
<span style={{ color: 'var(--dim-red)' }}>model={p.model}</span>
|
||||
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
||||
key={p.apiKey ? 'configured' : 'no key'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="section-header">Theme</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{themes.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
className={theme === t.id ? 'primary' : ''}
|
||||
onClick={() => handleThemeChange(t.id)}
|
||||
>
|
||||
{t.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="section-header">Skills ({skillList.length})</div>
|
||||
{skillList.length === 0 ? (
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 12 }}>No skills. Run `muyue skills init`.</span>
|
||||
) : (
|
||||
skillList.map((s, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<span className="tool-name">{s.name}</span>
|
||||
<span style={{ color: 'var(--cyber-red)', fontSize: 11 }}>[{s.target || 'both'}]</span>
|
||||
<span style={{ color: 'var(--dim-red)', fontSize: 11 }}>{s.description}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, value }) {
|
||||
return (
|
||||
<div className="config-field">
|
||||
<span className="config-label">{label}:</span>
|
||||
<span className="config-value">{value || '-'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
121
web/src/components/Dashboard.jsx
Normal file
121
web/src/components/Dashboard.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function Dashboard({ tools, updates, api, onRescan }) {
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [installLog, setInstallLog] = useState([])
|
||||
|
||||
const installed = tools.filter(t => t.installed).length
|
||||
const total = tools.length
|
||||
const pct = total > 0 ? (installed / total) * 100 : 0
|
||||
const missing = tools.filter(t => !t.installed).map(t => t.Name || t.name)
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (missing.length === 0) return
|
||||
setInstalling(true)
|
||||
setInstallLog(prev => [...prev, { text: `Installing ${missing.length} tools...`, type: 'info' }])
|
||||
try {
|
||||
await api.installTools(missing)
|
||||
setInstallLog(prev => [...prev, { text: 'Install started. Rescan to see changes.', type: 'ok' }])
|
||||
const data = await api.runScan()
|
||||
const toolData = await api.getTools()
|
||||
onRescan(toolData.tools || [])
|
||||
} catch (err) {
|
||||
setInstallLog(prev => [...prev, { text: err.message, type: 'error' }])
|
||||
}
|
||||
setInstalling(false)
|
||||
}
|
||||
|
||||
const handleScan = async () => {
|
||||
await api.runScan()
|
||||
const data = await api.getTools()
|
||||
onRescan(data.tools || [])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid-2">
|
||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
||||
<div className="section-header">System</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ color: 'var(--text-main)' }}>{installed}/{total} tools installed</span>
|
||||
</div>
|
||||
|
||||
<div className="section-header">Installed Tools</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
{tools.map((t, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<span className={`tool-status ${t.installed ? 'ok' : 'missing'}`}>
|
||||
{t.installed ? '[OK]' : '[--]'}
|
||||
</span>
|
||||
<span className="tool-name">{t.Name || t.name}</span>
|
||||
{(t.Version || t.version) && (
|
||||
<span className="tool-version">{extractVersion(t.Version || t.version)}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="progress-bar" style={{ marginBottom: 16 }}>
|
||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
{installing && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span className="loading-spinner"> Installing...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{installLog.length > 0 && (
|
||||
<div>
|
||||
<div className="section-header">Install Log</div>
|
||||
{installLog.map((log, i) => (
|
||||
<div key={i} style={{
|
||||
color: log.type === 'error' ? 'var(--error)' :
|
||||
log.type === 'ok' ? 'var(--success)' : 'var(--text-dim)',
|
||||
fontSize: 12, padding: '2px 0'
|
||||
}}>
|
||||
{log.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
||||
<div className="section-header">Quick Actions</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 16 }}>
|
||||
<button onClick={handleInstall} disabled={installing || missing.length === 0}>
|
||||
[i] Install missing ({missing.length})
|
||||
</button>
|
||||
<button onClick={() => api.getUpdates().then(d => {})}> [u] Check updates</button>
|
||||
<button onClick={handleScan}>[s] Rescan system</button>
|
||||
<button onClick={() => api.configureMCP()}>[m] Configure MCP</button>
|
||||
</div>
|
||||
|
||||
<div className="section-header">Updates</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{updates.length === 0 ? (
|
||||
<span style={{ color: 'var(--text-muted)' }}>No update data yet</span>
|
||||
) : updates.map((u, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<span className={`tool-status ${u.needsUpdate ? 'missing' : 'ok'}`}>
|
||||
{u.needsUpdate ? '[!!]' : '[OK]'}
|
||||
</span>
|
||||
<span className="tool-name">{u.tool}</span>
|
||||
{u.needsUpdate && (
|
||||
<span style={{ color: 'var(--warning)', fontSize: 11 }}>
|
||||
{u.current} → {u.latest}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function extractVersion(s) {
|
||||
if (!s) return ''
|
||||
const m = s.match(/\d+\.\d+\.\d+/)
|
||||
return m ? m[0] : s.slice(0, 12)
|
||||
}
|
||||
150
web/src/components/Shell.jsx
Normal file
150
web/src/components/Shell.jsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
export default function Shell({ api }) {
|
||||
const [history, setHistory] = useState([])
|
||||
const [input, setInput] = useState('')
|
||||
const [cwd, setCwd] = useState('~')
|
||||
const [aiPanel, setAiPanel] = useState(true)
|
||||
const [aiMessages, setAiMessages] = useState([
|
||||
{ role: 'ai', content: '>> I know your system inside out. Ask me anything.' }
|
||||
])
|
||||
const [aiInput, setAiInput] = useState('')
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const outputRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
outputRef.current?.scrollTo(0, outputRef.current.scrollHeight)
|
||||
}, [history])
|
||||
|
||||
const handleCommand = async (cmd) => {
|
||||
if (!cmd.trim()) return
|
||||
if (cmd === 'clear') {
|
||||
setHistory([])
|
||||
return
|
||||
}
|
||||
if (cmd === 'exit' || cmd === 'quit') return
|
||||
|
||||
setHistory(prev => [...prev, { type: 'input', text: `${cwd} $ ${cmd}` }])
|
||||
|
||||
try {
|
||||
const res = await api.runCommand(cmd, cwd === '~' ? '' : cwd)
|
||||
if (res.output) {
|
||||
setHistory(prev => [...prev, { type: 'output', text: res.output }])
|
||||
}
|
||||
if (res.error) {
|
||||
setHistory(prev => [...prev, { type: 'error', text: res.error }])
|
||||
}
|
||||
if (cmd.startsWith('cd ')) {
|
||||
const dir = cmd.slice(3).trim()
|
||||
setCwd(dir === '~' ? '~' : dir)
|
||||
}
|
||||
} catch (err) {
|
||||
setHistory(prev => [...prev, { type: 'error', text: err.message }])
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleCommand(input)
|
||||
setInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAiSend = async () => {
|
||||
if (!aiInput.trim() || aiLoading) return
|
||||
const text = aiInput.trim()
|
||||
setAiMessages(prev => [...prev, { role: 'user', content: '>> ' + text }])
|
||||
setAiInput('')
|
||||
setAiLoading(true)
|
||||
|
||||
try {
|
||||
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || 'No response') }])
|
||||
} catch (err) {
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }])
|
||||
}
|
||||
setAiLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="split-horizontal" style={{ height: '100%' }}>
|
||||
<div className="terminal-container" style={{ flex: 1 }}>
|
||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-dim)' }}>
|
||||
<div className="section-header" style={{ margin: 0 }}>
|
||||
Terminal
|
||||
<span style={{ color: 'var(--dim-red)', fontWeight: 400, marginLeft: 12, fontSize: 11 }}>{cwd}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="terminal-output" ref={outputRef}>
|
||||
{history.map((line, i) => (
|
||||
<div key={i} style={{
|
||||
color: line.type === 'input' ? 'var(--dim-red)' :
|
||||
line.type === 'error' ? 'var(--error)' : 'var(--text-main)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
{line.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="terminal-input-row">
|
||||
<span className="terminal-prompt">{'>'}</span>
|
||||
<input
|
||||
className="terminal-input"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiPanel && (
|
||||
<div style={{
|
||||
width: 320,
|
||||
borderLeft: '1px solid var(--border-dim)',
|
||||
background: 'var(--bg-surface)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-dim)' }}>
|
||||
<div className="section-header" style={{ margin: 0 }}>AI Assistant</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
|
||||
{aiMessages.map((msg, i) => (
|
||||
<div key={i} style={{
|
||||
marginBottom: 8,
|
||||
padding: '6px 8px',
|
||||
borderRadius: 'var(--radius)',
|
||||
background: msg.role === 'ai' ? 'var(--bg-card)' : 'var(--muted-red)',
|
||||
borderLeft: `3px solid ${msg.role === 'ai' ? 'var(--cyber-red)' : 'var(--cyber-rose)'}`,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
{msg.content}
|
||||
</div>
|
||||
))}
|
||||
{aiLoading && <span className="loading-spinner"> thinking...</span>}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '8px 12px', borderTop: '1px solid var(--border-dim)', display: 'flex', gap: 6 }}>
|
||||
<input
|
||||
style={{ flex: 1, padding: '4px 8px', fontSize: 12 }}
|
||||
value={aiInput}
|
||||
onChange={e => setAiInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
|
||||
placeholder="Ask AI..."
|
||||
/>
|
||||
<button style={{ padding: '4px 8px' }} onClick={handleAiSend}>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
126
web/src/components/Studio.jsx
Normal file
126
web/src/components/Studio.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
export default function Studio({ api }) {
|
||||
const [messages, setMessages] = useState([
|
||||
{ role: 'ai', content: '>> Welcome to Studio! Chat with your AI assistant here.' },
|
||||
{ role: 'ai', content: '>> Configure agents and workflows from the sidebar.' },
|
||||
])
|
||||
const [input, setInput] = useState('')
|
||||
const [sidebarPanel, setSidebarPanel] = useState('chat')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const messagesEnd = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || loading) return
|
||||
const text = input.trim()
|
||||
setMessages(prev => [...prev, { role: 'user', content: '>> ' + text }])
|
||||
setInput('')
|
||||
setLoading(true)
|
||||
|
||||
api.runCommand(`echo "AI response simulation for: ${text}"`, '')
|
||||
.then(res => {
|
||||
setMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || res.error || 'No response') }])
|
||||
})
|
||||
.catch(err => {
|
||||
setMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }])
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="split-horizontal">
|
||||
<div className="chat-container" style={{ flex: 1, borderRight: '1px solid var(--border-dim)' }}>
|
||||
<div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-dim)' }}>
|
||||
<div className="section-header" style={{ margin: 0 }}>
|
||||
Chat
|
||||
{loading && <span className="loading-spinner" style={{ marginLeft: 8 }}> thinking...</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="chat-messages">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`chat-message ${msg.role}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEnd} />
|
||||
</div>
|
||||
|
||||
<div className="chat-input-container">
|
||||
<input
|
||||
className="chat-input"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message... (/plan <goal> for workflows)"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button className="primary" onClick={handleSend} disabled={loading || !input.trim()}>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="split-right">
|
||||
<div className="sidebar-section">
|
||||
<div className="section-header">Studio</div>
|
||||
{['chat', 'agents', 'workflows'].map(panel => (
|
||||
<div
|
||||
key={panel}
|
||||
className={`sidebar-item ${sidebarPanel === panel ? 'active' : ''}`}
|
||||
onClick={() => setSidebarPanel(panel)}
|
||||
>
|
||||
[{panel === 'chat' ? '#' : panel === 'agents' ? '*' : '~'}] {panel.charAt(0).toUpperCase() + panel.slice(1)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: '1px solid var(--border-dim)', paddingTop: 12 }}>
|
||||
{sidebarPanel === 'chat' && (
|
||||
<div>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12, marginBottom: 8 }}>Commands</div>
|
||||
<div style={{ color: 'var(--dim-red)', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
|
||||
/plan {'<goal>'}<br/>
|
||||
/help
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sidebarPanel === 'agents' && (
|
||||
<div>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12, marginBottom: 8 }}>Active Agents</div>
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--text-main)', fontWeight: 600 }}>Crush</span>
|
||||
<span style={{ color: 'var(--text-muted)', marginLeft: 8, fontSize: 11 }}>[|| stopped]</span>
|
||||
</div>
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--text-main)', fontWeight: 600 }}>Claude Code</span>
|
||||
<span style={{ color: 'var(--text-muted)', marginLeft: 8, fontSize: 11 }}>[|| stopped]</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sidebarPanel === 'workflows' && (
|
||||
<div>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No active workflow.</div>
|
||||
<div style={{ color: 'var(--dim-red)', fontSize: 12, marginTop: 8 }}>
|
||||
Use /plan {'<goal>'} in chat to start.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
web/src/main.jsx
Normal file
9
web/src/main.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './components/App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
576
web/src/styles/global.css
Normal file
576
web/src/styles/global.css
Normal file
@@ -0,0 +1,576 @@
|
||||
:root {
|
||||
--bg-void: #0A0A0C;
|
||||
--bg-base: #0F0D10;
|
||||
--bg-surface: #161218;
|
||||
--bg-panel: #1C1719;
|
||||
--bg-card: #221B1E;
|
||||
--bg-input: #2A2225;
|
||||
--bg-hover: #332528;
|
||||
|
||||
--cyber-red: #FF0033;
|
||||
--cyber-red-dark: #8B0020;
|
||||
--cyber-red-deep: #5C0015;
|
||||
--cyber-pink: #FF1A5E;
|
||||
--cyber-rose: #FF4D6D;
|
||||
--neon-red: #FF1744;
|
||||
--bright-red: #FF5252;
|
||||
--dim-red: #6B2033;
|
||||
--muted-red: #4A1525;
|
||||
|
||||
--text-bright: #EAE0E2;
|
||||
--text-main: #D4C4C8;
|
||||
--text-dim: #8A7A7E;
|
||||
--text-muted: #5A4F52;
|
||||
|
||||
--success: #00E676;
|
||||
--warning: #FFD740;
|
||||
--error: #FF1744;
|
||||
|
||||
--border-dim: #2A1F22;
|
||||
--border-red: #FF003344;
|
||||
--border-red-full: #FF0033;
|
||||
|
||||
--radius: 8px;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace;
|
||||
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
|
||||
--header-h: 48px;
|
||||
--footer-h: 32px;
|
||||
--tab-h: 40px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-void);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--dim-red);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--cyber-red-dark);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--cyber-red);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--cyber-red);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--cyber-pink);
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
border-color: var(--cyber-red);
|
||||
box-shadow: 0 0 0 2px var(--border-red);
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-main);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--cyber-red-dark);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--cyber-red);
|
||||
color: #fff;
|
||||
border-color: var(--cyber-red);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: var(--neon-red);
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: var(--header-h);
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--cyber-red);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.header-version {
|
||||
font-size: 11px;
|
||||
color: var(--dim-red);
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.header-tab {
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
text-transform: uppercase;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.header-tab:hover {
|
||||
color: var(--text-main);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.header-tab.active {
|
||||
color: #fff;
|
||||
background: var(--cyber-red);
|
||||
border-color: var(--cyber-red);
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-dot.ok { background: var(--success); }
|
||||
.status-dot.warn { background: var(--warning); }
|
||||
.status-dot.error { background: var(--error); }
|
||||
.status-dot.off { background: var(--text-muted); }
|
||||
|
||||
.header-clock {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--cyber-red);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-date {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: var(--footer-h);
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-shortcuts {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.footer-shortcuts kbd {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
font-size: 11px;
|
||||
color: var(--dim-red);
|
||||
}
|
||||
|
||||
.footer-update {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.footer-update.available {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.footer-update.uptodate {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--dim-red);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--cyber-red);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--cyber-red);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.section-header::before {
|
||||
content: '■';
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.tool-status.ok { color: var(--success); }
|
||||
.tool-status.missing { color: var(--error); }
|
||||
|
||||
.tool-name {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.tool-version {
|
||||
color: var(--dim-red);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: var(--bg-input);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--cyber-red), var(--cyber-pink));
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.split-horizontal {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.split-left {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.split-right {
|
||||
width: 320px;
|
||||
border-left: 1px solid var(--border-dim);
|
||||
background: var(--bg-surface);
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chat-message.ai {
|
||||
background: var(--bg-card);
|
||||
border-left: 3px solid var(--cyber-red);
|
||||
}
|
||||
|
||||
.chat-message.user {
|
||||
background: var(--muted-red);
|
||||
border-left: 3px solid var(--cyber-rose);
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
background: var(--cyber-red);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
background: var(--bg-void);
|
||||
}
|
||||
|
||||
.terminal-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-input);
|
||||
border-top: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.terminal-prompt {
|
||||
color: var(--success);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.config-label {
|
||||
width: 140px;
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-value {
|
||||
color: var(--text-main);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
0% { transform: translate(0); }
|
||||
20% { transform: translate(-2px, 2px); }
|
||||
40% { transform: translate(-2px, -2px); }
|
||||
60% { transform: translate(2px, 2px); }
|
||||
80% { transform: translate(2px, -2px); }
|
||||
100% { transform: translate(0); }
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
0% { top: -10%; }
|
||||
100% { top: 110%; }
|
||||
}
|
||||
|
||||
@keyframes typewriter {
|
||||
from { width: 0; }
|
||||
to { width: 100%; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.tab-transition {
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.glitch-text {
|
||||
animation: glitch 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
animation: pulse 1s infinite;
|
||||
color: var(--cyber-red);
|
||||
}
|
||||
138
web/src/themes/index.js
Normal file
138
web/src/themes/index.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const defaultTheme = {
|
||||
name: 'Cyberpunk Red',
|
||||
colors: {
|
||||
bgVoid: '#0A0A0C',
|
||||
bgBase: '#0F0D10',
|
||||
bgSurface: '#161218',
|
||||
bgPanel: '#1C1719',
|
||||
bgCard: '#221B1E',
|
||||
bgInput: '#2A2225',
|
||||
bgHover: '#332528',
|
||||
cyberRed: '#FF0033',
|
||||
cyberRedDark: '#8B0020',
|
||||
cyberRedDeep: '#5C0015',
|
||||
cyberPink: '#FF1A5E',
|
||||
cyberRose: '#FF4D6D',
|
||||
neonRed: '#FF1744',
|
||||
brightRed: '#FF5252',
|
||||
dimRed: '#6B2033',
|
||||
mutedRed: '#4A1525',
|
||||
textBright: '#EAE0E2',
|
||||
textMain: '#D4C4C8',
|
||||
textDim: '#8A7A7E',
|
||||
textMuted: '#5A4F52',
|
||||
success: '#00E676',
|
||||
warning: '#FFD740',
|
||||
error: '#FF1744',
|
||||
borderDim: '#2A1F22',
|
||||
borderRed: '#FF003344',
|
||||
borderRedFull: '#FF0033',
|
||||
},
|
||||
fonts: {
|
||||
mono: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
|
||||
ui: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
},
|
||||
borderRadius: '8px',
|
||||
animations: {
|
||||
glitch: true,
|
||||
scanline: true,
|
||||
typewriter: true,
|
||||
pulse: true,
|
||||
},
|
||||
}
|
||||
|
||||
const themes = {
|
||||
'cyberpunk-red': defaultTheme,
|
||||
'cyberpunk-pink': {
|
||||
...defaultTheme,
|
||||
name: 'Cyberpunk Pink',
|
||||
colors: {
|
||||
...defaultTheme.colors,
|
||||
cyberRed: '#FF1A8C',
|
||||
cyberRedDark: '#8B1050',
|
||||
cyberRedDeep: '#5C0A35',
|
||||
cyberPink: '#FF4DAE',
|
||||
cyberRose: '#FF6DC2',
|
||||
neonRed: '#FF1A8C',
|
||||
brightRed: '#FF6DC2',
|
||||
dimRed: '#6B2050',
|
||||
mutedRed: '#4A1535',
|
||||
},
|
||||
},
|
||||
'midnight-blue': {
|
||||
...defaultTheme,
|
||||
name: 'Midnight Blue',
|
||||
colors: {
|
||||
...defaultTheme.colors,
|
||||
cyberRed: '#0088FF',
|
||||
cyberRedDark: '#004488',
|
||||
cyberRedDeep: '#002255',
|
||||
cyberPink: '#00AAFF',
|
||||
cyberRose: '#44CCFF',
|
||||
neonRed: '#0088FF',
|
||||
brightRed: '#44CCFF',
|
||||
dimRed: '#203366',
|
||||
mutedRed: '#152244',
|
||||
},
|
||||
},
|
||||
'matrix-green': {
|
||||
...defaultTheme,
|
||||
name: 'Matrix Green',
|
||||
colors: {
|
||||
...defaultTheme.colors,
|
||||
cyberRed: '#00FF41',
|
||||
cyberRedDark: '#008822',
|
||||
cyberRedDeep: '#005515',
|
||||
cyberPink: '#33FF66',
|
||||
cyberRose: '#66FF99',
|
||||
neonRed: '#00FF41',
|
||||
brightRed: '#66FF99',
|
||||
dimRed: '#206630',
|
||||
mutedRed: '#154420',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function getTheme(name) {
|
||||
return themes[name] || defaultTheme
|
||||
}
|
||||
|
||||
export function getThemeNames() {
|
||||
return Object.keys(themes).map(k => ({ id: k, name: themes[k].name }))
|
||||
}
|
||||
|
||||
export function applyTheme(theme) {
|
||||
const root = document.documentElement
|
||||
const c = theme.colors
|
||||
const map = {
|
||||
'--bg-void': c.bgVoid,
|
||||
'--bg-base': c.bgBase,
|
||||
'--bg-surface': c.bgSurface,
|
||||
'--bg-panel': c.bgPanel,
|
||||
'--bg-card': c.bgCard,
|
||||
'--bg-input': c.bgInput,
|
||||
'--bg-hover': c.bgHover,
|
||||
'--cyber-red': c.cyberRed,
|
||||
'--cyber-red-dark': c.cyberRedDark,
|
||||
'--cyber-red-deep': c.cyberRedDeep,
|
||||
'--cyber-pink': c.cyberPink,
|
||||
'--cyber-rose': c.cyberRose,
|
||||
'--neon-red': c.neonRed,
|
||||
'--bright-red': c.brightRed,
|
||||
'--dim-red': c.dimRed,
|
||||
'--muted-red': c.mutedRed,
|
||||
'--text-bright': c.textBright,
|
||||
'--text-main': c.textMain,
|
||||
'--text-dim': c.textDim,
|
||||
'--text-muted': c.textMuted,
|
||||
'--success': c.success,
|
||||
'--warning': c.warning,
|
||||
'--error': c.error,
|
||||
'--border-dim': c.borderDim,
|
||||
'--border-red': c.borderRed,
|
||||
'--border-red-full': c.borderRedFull,
|
||||
}
|
||||
Object.entries(map).forEach(([k, v]) => root.style.setProperty(k, v))
|
||||
}
|
||||
|
||||
export default defaultTheme
|
||||
19
web/vite.config.js
Normal file
19
web/vite.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8095',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user