Compare commits
12 Commits
v0.3.1-bet
...
v0.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b4a70e690 | ||
|
|
95bd824259 | ||
|
|
252f178bbd | ||
|
|
7dcf505360 | ||
|
|
8fb93fa47e | ||
|
|
5ec373cd6a | ||
|
|
1eb5a6d00f | ||
|
|
cd5ebe083c | ||
|
|
2004c15dd7 | ||
|
|
9306152736 | ||
|
|
e15a034de5 | ||
|
|
80c11cab3f |
114
CHANGELOG.md
114
CHANGELOG.md
@@ -4,6 +4,120 @@ 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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## v0.3.1
|
||||||
|
|
||||||
|
### Changes since v0.3.0
|
||||||
|
|
||||||
|
- refactor(config): remove Terminal sub-tab from Configuration page (95bd824)
|
||||||
|
- fix(terminal): init payload never sent due to ws.onopen being overwritten (252f178)
|
||||||
|
- fix(terminal): improve shell resolution with better error handling and ws proxy support (7dcf505)
|
||||||
|
- feat(studio): parse AI thinking and tool launch messages in terminal panel (8fb93fa)
|
||||||
|
- fix(studio): forward AI thinking chunks to frontend instead of dropping them (5ec373c)
|
||||||
|
- feat(studio): add tool execution and hide AI thinking tags (1eb5a6d)
|
||||||
|
- fix(terminal): ignore invalid shell config from race condition (cd5ebe0)
|
||||||
|
- feat(shell): restore AI assistant panel (2004c15)
|
||||||
|
- fix(terminal): restore terminal input and cursor visibility (9306152)
|
||||||
|
- refactor(api): split monolithic handlers.go into focused modules (e15a034)
|
||||||
|
|
||||||
|
### Downloads
|
||||||
|
|
||||||
|
| Platform | File |
|
||||||
|
|----------|------|
|
||||||
|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-linux-amd64.tar.gz) |
|
||||||
|
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-linux-arm64.tar.gz) |
|
||||||
|
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-darwin-amd64.tar.gz) |
|
||||||
|
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-darwin-arm64.tar.gz) |
|
||||||
|
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-windows-amd64.zip) |
|
||||||
|
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-windows-arm64.zip) |
|
||||||
|
|
||||||
|
The binary includes both CLI and Desktop modes.
|
||||||
|
Run `muyue` for TUI, `muyue desktop` for web UI.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
**Linux (x86_64)**
|
||||||
|
```bash
|
||||||
|
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.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.3.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.3.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.3.0
|
||||||
|
|
||||||
|
### Changes since v0.2.1
|
||||||
|
|
||||||
|
- fix(terminal): resolve PTY shell exec error, simplify CLI, unify Config tabs, restore Studio CSS (0b22109)
|
||||||
|
- feat: add API key validation flow for AI provider config (7f67473)
|
||||||
|
- feat(studio): replace sidebar layout with unified execution feed styles (040e482)
|
||||||
|
- fix: guard against empty tabs array in closeTab (c8903ef)
|
||||||
|
- refactor: redesign Config as settings window with sidebar panels, remove system overview from Dashboard (f3cb306)
|
||||||
|
- feat: add multi-tab terminal with SSH support, config editing, and dashboard redesign (3cdcb22)
|
||||||
|
- feat(studio): add i18n keys and CSS for redesigned AI chat interface (ee18bbe)
|
||||||
|
- chore: bump version to 0.3.0 (b0b0e1d)
|
||||||
|
- chore: remove dead code (packages, functions, types, constants) (fc79810)
|
||||||
|
- docs: rewrite README and CHANGELOG for desktop app mode (f7222b0)
|
||||||
|
- feat(web): add i18n support with FR/EN locales and keyboard layout awareness (11417d3)
|
||||||
|
- refactor(web): redesign frontend for native web UX (3dc24ae)
|
||||||
|
- refactor: remove TUI, desktop web UI is now the default and only mode (aa0ff19)
|
||||||
|
- refactor: unify into single `muyue` binary with embedded desktop mode (3463605)
|
||||||
|
- fix(ci): add frontend build step before Go vet/test/build (097cf40)
|
||||||
|
- feat: add desktop app with React frontend, API backend, theme system (#2) (88d2a03)
|
||||||
|
- chore: update CHANGELOG for v0.2.1 (1830c18)
|
||||||
|
- 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.3.0/muyue-linux-amd64.tar.gz) |
|
||||||
|
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-linux-arm64.tar.gz) |
|
||||||
|
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-darwin-amd64.tar.gz) |
|
||||||
|
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-darwin-arm64.tar.gz) |
|
||||||
|
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-windows-amd64.zip) |
|
||||||
|
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-windows-arm64.zip) |
|
||||||
|
|
||||||
|
The binary includes both CLI and Desktop modes.
|
||||||
|
Run `muyue` for TUI, `muyue desktop` for web UI.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
**Linux (x86_64)**
|
||||||
|
```bash
|
||||||
|
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.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.3.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.3.0/muyue-windows-amd64.zip" -OutFile "muyue.zip"
|
||||||
|
Expand-Archive -Path "muyue.zip" -DestinationPath "."
|
||||||
|
Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
@@ -73,8 +73,20 @@ RÈGLES ABSOLUES:
|
|||||||
flusher, canFlush := w.(http.Flusher)
|
flusher, canFlush := w.(http.Flusher)
|
||||||
|
|
||||||
result, err := orb.SendStream(body.Message, func(chunk string) {
|
result, err := orb.SendStream(body.Message, func(chunk string) {
|
||||||
// Skip thinking tags - user doesn't see them
|
|
||||||
if strings.HasPrefix(chunk, "<think") {
|
if strings.HasPrefix(chunk, "<think") {
|
||||||
|
data, _ := json.Marshal(map[string]string{"thinking": strings.TrimPrefix(chunk, "<think")})
|
||||||
|
w.Write([]byte("data: " + string(data) + "\n\n"))
|
||||||
|
if canFlush {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if chunk == "</think>" {
|
||||||
|
data, _ := json.Marshal(map[string]string{"thinking_end": "true"})
|
||||||
|
w.Write([]byte("data: " + string(data) + "\n\n"))
|
||||||
|
if canFlush {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(map[string]string{"content": chunk})
|
data, _ := json.Marshal(map[string]string{"content": chunk})
|
||||||
|
|||||||
@@ -56,13 +56,17 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
var initMsg wsMessage
|
var initMsg wsMessage
|
||||||
_, raw, err := conn.ReadMessage()
|
_, raw, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("terminal: read init message failed: %v", err)
|
||||||
conn.WriteJSON(wsMessage{Type: "error", Data: "failed to read init message"})
|
conn.WriteJSON(wsMessage{Type: "error", Data: "failed to read init message"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("terminal: init message received: %s", string(raw))
|
||||||
if err := json.Unmarshal(raw, &initMsg); err != nil {
|
if err := json.Unmarshal(raw, &initMsg); err != nil {
|
||||||
|
log.Printf("terminal: unmarshal init message failed: %v", err)
|
||||||
conn.WriteJSON(wsMessage{Type: "error", Data: "invalid init message"})
|
conn.WriteJSON(wsMessage{Type: "error", Data: "invalid init message"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("terminal: init type=%q data=%q", initMsg.Type, initMsg.Data)
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
@@ -96,23 +100,26 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
cmd = exec.Command("ssh", sshArgs...)
|
cmd = exec.Command("ssh", sshArgs...)
|
||||||
} else {
|
} else {
|
||||||
shell := initMsg.Data
|
shell := strings.TrimSpace(initMsg.Data)
|
||||||
|
log.Printf("terminal: requested shell=%q, trimmed=%q", initMsg.Data, shell)
|
||||||
if shell == "" {
|
if shell == "" {
|
||||||
shell = detectShell()
|
shell = detectShell()
|
||||||
} else {
|
log.Printf("terminal: auto-detected shell=%q", shell)
|
||||||
if path, err := exec.LookPath(shell); err == nil {
|
|
||||||
shell = path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore invalid shell paths (e.g., single characters from race condition)
|
if shell == "" {
|
||||||
if len(shell) <= 1 {
|
log.Printf("terminal: no shell detected, falling back to /bin/sh")
|
||||||
conn.WriteJSON(wsMessage{Type: "error", Data: "invalid shell config"})
|
shell = "/bin/sh"
|
||||||
return
|
}
|
||||||
|
|
||||||
|
if path, err := exec.LookPath(shell); err == nil {
|
||||||
|
shell = path
|
||||||
|
log.Printf("terminal: resolved shell path=%q", shell)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(shell); err != nil {
|
if _, err := os.Stat(shell); err != nil {
|
||||||
conn.WriteJSON(wsMessage{Type: "error", Data: fmt.Sprintf("shell not found: %s", shell)})
|
log.Printf("terminal: shell stat failed: %v for %q", err, shell)
|
||||||
|
conn.WriteJSON(wsMessage{Type: "error", Data: fmt.Sprintf("shell not found: %s (resolved from: %q)", shell, initMsg.Data)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,12 +138,14 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
|
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
|
||||||
|
|
||||||
|
log.Printf("terminal: starting pty with cmd=%q args=%v", cmd.Path, cmd.Args)
|
||||||
ptmx, err := pty.Start(cmd)
|
ptmx, err := pty.Start(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("pty start: %v", err)
|
log.Printf("terminal: pty start failed: %v", err)
|
||||||
conn.WriteJSON(wsMessage{Type: "error", Data: err.Error()})
|
conn.WriteJSON(wsMessage{Type: "error", Data: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("terminal: pty started successfully")
|
||||||
defer func() {
|
defer func() {
|
||||||
ptmx.Close()
|
ptmx.Close()
|
||||||
if cmd.Process != nil {
|
if cmd.Process != nil {
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ const api = {
|
|||||||
if (data.done) { resolve(full); return }
|
if (data.done) { resolve(full); return }
|
||||||
if (data.content) {
|
if (data.content) {
|
||||||
full += data.content
|
full += data.content
|
||||||
if (onChunk) onChunk(full)
|
if (onChunk) onChunk(full, data)
|
||||||
|
} else if (data.thinking !== undefined || data.thinking_end) {
|
||||||
|
if (onChunk) onChunk(full, data)
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { User, Brain, RefreshCw, Globe, Wrench, Monitor } from 'lucide-react'
|
import { User, Brain, RefreshCw, Globe, Wrench } from 'lucide-react'
|
||||||
import { useI18n, LANGUAGES } from '../i18n'
|
import { useI18n, LANGUAGES } from '../i18n'
|
||||||
import { getLayoutList } from '../i18n/keyboards'
|
import { getLayoutList } from '../i18n/keyboards'
|
||||||
|
|
||||||
@@ -27,9 +27,7 @@ export default function Config({ api }) {
|
|||||||
const [profileForm, setProfileForm] = useState({})
|
const [profileForm, setProfileForm] = useState({})
|
||||||
const [providerForm, setProviderForm] = useState({})
|
const [providerForm, setProviderForm] = useState({})
|
||||||
const [toast, setToast] = useState(null)
|
const [toast, setToast] = useState(null)
|
||||||
const [terminalThemes, setTerminalThemes] = useState([])
|
|
||||||
const [terminalSettings, setTerminalSettings] = useState({ font_size: 14, font_family: '', theme: 'default' })
|
|
||||||
const [savingTerminal, setSavingTerminal] = useState(false)
|
|
||||||
|
|
||||||
const layouts = getLayoutList()
|
const layouts = getLayoutList()
|
||||||
|
|
||||||
@@ -43,19 +41,13 @@ export default function Config({ api }) {
|
|||||||
editor: d.profile?.preferences?.editor || '',
|
editor: d.profile?.preferences?.editor || '',
|
||||||
shell: d.profile?.preferences?.shell || '',
|
shell: d.profile?.preferences?.shell || '',
|
||||||
})
|
})
|
||||||
if (d.terminal) {
|
|
||||||
setTerminalSettings({
|
|
||||||
font_size: d.terminal.font_size || 14,
|
|
||||||
font_family: d.terminal.font_family || '',
|
|
||||||
theme: d.terminal.theme || 'default',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {})
|
api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {})
|
||||||
api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {})
|
api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {})
|
||||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||||
api.getTerminalThemes().then(d => setTerminalThemes(d.themes || [])).catch(() => {})
|
|
||||||
}, [api])
|
}, [api])
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [loadData])
|
useEffect(() => { loadData() }, [loadData])
|
||||||
@@ -126,18 +118,6 @@ export default function Config({ api }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveTerminalSettings = async () => {
|
|
||||||
setSavingTerminal(true)
|
|
||||||
try {
|
|
||||||
await api.saveTerminalSettings(terminalSettings)
|
|
||||||
showToast(t('config.saved'))
|
|
||||||
window.location.reload()
|
|
||||||
} catch (err) {
|
|
||||||
showToast(`${t('config.error')}: ${err.message}`)
|
|
||||||
}
|
|
||||||
setSavingTerminal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openProviderEdit = (p) => {
|
const openProviderEdit = (p) => {
|
||||||
setProviderForm({
|
setProviderForm({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
@@ -213,13 +193,7 @@ export default function Config({ api }) {
|
|||||||
{activePanel === 'skills' && (
|
{activePanel === 'skills' && (
|
||||||
<PanelSkills skillList={skillList} t={t} />
|
<PanelSkills skillList={skillList} t={t} />
|
||||||
)}
|
)}
|
||||||
{activePanel === 'terminal' && (
|
|
||||||
<PanelTerminal
|
|
||||||
settings={terminalSettings} setSettings={setTerminalSettings}
|
|
||||||
themes={terminalThemes} saving={savingTerminal}
|
|
||||||
onSave={handleSaveTerminalSettings} t={t}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -470,102 +444,6 @@ function PanelSkills({ skillList, t }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelTerminal({ settings, setSettings, themes, saving, onSave, t }) {
|
|
||||||
const previewTheme = {
|
|
||||||
background: settings.theme === 'default' ? '#0A0A0C' :
|
|
||||||
settings.theme === 'monokai' ? '#272822' :
|
|
||||||
settings.theme === 'gruvbox' ? '#282828' :
|
|
||||||
settings.theme === 'nord' ? '#2E3440' :
|
|
||||||
settings.theme === 'solarized-dark' ? '#002B36' :
|
|
||||||
settings.theme === 'dracula' ? '#282A36' : '#0A0A0C',
|
|
||||||
foreground: settings.theme === 'default' ? '#EAE0E2' :
|
|
||||||
settings.theme === 'monokai' ? '#F8F8F2' :
|
|
||||||
settings.theme === 'gruvbox' ? '#EBDBB2' :
|
|
||||||
settings.theme === 'nord' ? '#D8DEE9' :
|
|
||||||
settings.theme === 'solarized-dark' ? '#839496' :
|
|
||||||
settings.theme === 'dracula' ? '#F8F8F2' : '#EAE0E2',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="config-card">
|
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.terminalTheme')}</span>
|
|
||||||
<div className="chip-row">
|
|
||||||
{themes.map(th => (
|
|
||||||
<div
|
|
||||||
key={th.id}
|
|
||||||
className={`chip ${settings.theme === th.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSettings(s => ({ ...s, theme: th.id }))}
|
|
||||||
>
|
|
||||||
{th.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.fontSize')}</span>
|
|
||||||
<div className="chip-row">
|
|
||||||
{[12, 14, 16, 18, 20, 24].map(size => (
|
|
||||||
<div
|
|
||||||
key={size}
|
|
||||||
className={`chip ${settings.font_size === size ? 'active' : ''}`}
|
|
||||||
onClick={() => setSettings(s => ({ ...s, font_size: size }))}
|
|
||||||
>
|
|
||||||
{size}px
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.fontFamily')}</span>
|
|
||||||
<select
|
|
||||||
className="config-form-input"
|
|
||||||
value={settings.font_family}
|
|
||||||
onChange={e => setSettings(s => ({ ...s, font_family: e.target.value }))}
|
|
||||||
style={{ maxWidth: 300 }}
|
|
||||||
>
|
|
||||||
<option value="">Default (JetBrains Mono)</option>
|
|
||||||
<option value="'Fira Code', monospace">Fira Code</option>
|
|
||||||
<option value="'Cascadia Code', 'SF Mono', monospace">Cascadia Code</option>
|
|
||||||
<option value="'SF Mono', 'Menlo', monospace">SF Mono</option>
|
|
||||||
<option value="'Source Code Pro', monospace">Source Code Pro</option>
|
|
||||||
<option value="monospace">System Monospace</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.preview')}</span>
|
|
||||||
<div style={{
|
|
||||||
background: previewTheme.background,
|
|
||||||
color: previewTheme.foreground,
|
|
||||||
padding: '16px 20px',
|
|
||||||
borderRadius: 'var(--radius)',
|
|
||||||
fontFamily: settings.font_family || "'JetBrains Mono', monospace",
|
|
||||||
fontSize: settings.font_size || 14,
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
}}>
|
|
||||||
<span style={{ color: '#00E676' }}>➜</span> <span>~/projects</span>
|
|
||||||
<span style={{ color: '#448AFF' }}> git status</span>
|
|
||||||
<br />
|
|
||||||
<span>On branch </span>
|
|
||||||
<span style={{ color: '#FFD740' }}>main</span>
|
|
||||||
<br />
|
|
||||||
<span style={{ opacity: 0.6 }}>Type a command...</span>
|
|
||||||
<span style={{ animation: 'blink 1s step-end infinite' }}> ▋</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="config-card-actions" style={{ marginTop: 16 }}>
|
|
||||||
<button className="primary sm" onClick={onSave} disabled={saving}>
|
|
||||||
{saving ? t('config.saving') : t('config.save')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormInput({ label, value, onChange, type = 'text' }) {
|
function FormInput({ label, value, onChange, type = 'text' }) {
|
||||||
return (
|
return (
|
||||||
<div className="config-form-field">
|
<div className="config-form-field">
|
||||||
|
|||||||
@@ -94,15 +94,15 @@ function connectWebSocket(term, fitAddon, initPayload) {
|
|||||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
const ws = new WebSocket(`${proto}//${window.location.host}/api/ws/terminal`)
|
const ws = new WebSocket(`${proto}//${window.location.host}/api/ws/terminal`)
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.addEventListener('open', () => {
|
||||||
ws.send(JSON.stringify(initPayload))
|
ws.send(JSON.stringify(initPayload))
|
||||||
const dims = fitAddon.proposeDimensions()
|
const dims = fitAddon.proposeDimensions()
|
||||||
if (dims) {
|
if (dims) {
|
||||||
ws.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols }))
|
ws.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols }))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.addEventListener('message', (event) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
if (msg.type === 'output') {
|
if (msg.type === 'output') {
|
||||||
@@ -113,15 +113,15 @@ function connectWebSocket(term, fitAddon, initPayload) {
|
|||||||
} catch {
|
} catch {
|
||||||
term.write(event.data)
|
term.write(event.data)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.addEventListener('close', () => {
|
||||||
term.write('\r\n\x1b[33m— Connection closed —\x1b[0m\r\n')
|
term.write('\r\n\x1b[33m— Connection closed —\x1b[0m\r\n')
|
||||||
}
|
})
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.addEventListener('error', () => {
|
||||||
term.write('\r\n\x1b[31m— Connection error —\x1b[0m\r\n')
|
term.write('\r\n\x1b[31m— Connection error —\x1b[0m\r\n')
|
||||||
}
|
})
|
||||||
|
|
||||||
term.onData((data) => {
|
term.onData((data) => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
@@ -380,13 +380,61 @@ export default function Shell({ api }) {
|
|||||||
setAiLoading(true)
|
setAiLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
||||||
setAiMessages(prev => [...prev, { role: 'ai', content: res.output || t('shell.noResponse') }])
|
const output = res.output || t('shell.noResponse')
|
||||||
|
parseAndAddAiMessages(output)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setAiMessages(prev => [...prev, { role: 'ai', content: `${t('shell.error')}: ${err.message}` }])
|
setAiMessages(prev => [...prev, { role: 'ai', content: `${t('shell.error')}: ${err.message}` }])
|
||||||
}
|
}
|
||||||
setAiLoading(false)
|
setAiLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseAndAddAiMessages = (text) => {
|
||||||
|
const lines = text.split('\n')
|
||||||
|
let buffer = ''
|
||||||
|
let inBlock = false
|
||||||
|
|
||||||
|
const flushBuffer = () => {
|
||||||
|
if (buffer.trim()) {
|
||||||
|
setAiMessages(prev => [...prev, { role: 'ai', content: buffer.trim() }])
|
||||||
|
}
|
||||||
|
buffer = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const toolMatch = line.match(/^\[TOOL_CALL:\{.*\}\]$/)
|
||||||
|
if (toolMatch) {
|
||||||
|
flushBuffer()
|
||||||
|
try {
|
||||||
|
const toolData = JSON.parse(toolMatch[0].slice(10, -1))
|
||||||
|
setAiMessages(prev => [...prev, {
|
||||||
|
role: 'tool',
|
||||||
|
content: `${t('shell.toolLaunched')}: ${toolData.tool || 'tool'}`,
|
||||||
|
args: toolData.task || toolData.args || '',
|
||||||
|
}])
|
||||||
|
} catch {
|
||||||
|
setAiMessages(prev => [...prev, { role: 'tool', content: line, args: '' }])
|
||||||
|
}
|
||||||
|
} else if (line.match(/^(Reflexion|Thought|thinking):/i) || line.startsWith('>')) {
|
||||||
|
if (buffer.trim() && !inBlock) {
|
||||||
|
flushBuffer()
|
||||||
|
}
|
||||||
|
inBlock = true
|
||||||
|
const cleaned = line.replace(/^(Reflexion|Thought|thinking):\s*/i, '').replace(/^>\s*/, '')
|
||||||
|
if (buffer) buffer += ' '
|
||||||
|
buffer += cleaned
|
||||||
|
} else {
|
||||||
|
if (inBlock && buffer.trim()) {
|
||||||
|
setAiMessages(prev => [...prev, { role: 'thinking', content: buffer.trim() }])
|
||||||
|
buffer = ''
|
||||||
|
}
|
||||||
|
inBlock = false
|
||||||
|
if (buffer) buffer += '\n'
|
||||||
|
buffer += line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shell-layout">
|
<div className="shell-layout">
|
||||||
<div className="shell-terminal-col">
|
<div className="shell-terminal-col">
|
||||||
@@ -507,6 +555,7 @@ export default function Shell({ api }) {
|
|||||||
{aiMessages.map((msg, i) => (
|
{aiMessages.map((msg, i) => (
|
||||||
<div key={i} className={`ai-message ${msg.role}`}>
|
<div key={i} className={`ai-message ${msg.role}`}>
|
||||||
{msg.content}
|
{msg.content}
|
||||||
|
{msg.args && <div className="tool-args">{msg.args}</div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ const en = {
|
|||||||
aiAssistant: 'AI Assistant',
|
aiAssistant: 'AI Assistant',
|
||||||
aiWelcome: 'Hello! I can help you with terminal commands. Ask me anything!',
|
aiWelcome: 'Hello! I can help you with terminal commands. Ask me anything!',
|
||||||
askAi: 'Ask AI assistant...',
|
askAi: 'Ask AI assistant...',
|
||||||
|
toolLaunched: 'Tool launched',
|
||||||
},
|
},
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ const fr = {
|
|||||||
aiAssistant: 'Assistant IA',
|
aiAssistant: 'Assistant IA',
|
||||||
aiWelcome: 'Bonjour ! Je peux vous aider avec les commandes du terminal. Demandez-moi n\'importe quoi !',
|
aiWelcome: 'Bonjour ! Je peux vous aider avec les commandes du terminal. Demandez-moi n\'importe quoi !',
|
||||||
askAi: 'Interroger l\'assistant IA...',
|
askAi: 'Interroger l\'assistant IA...',
|
||||||
|
toolLaunched: 'Outil lanc\u00e9',
|
||||||
},
|
},
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -391,6 +391,10 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
.ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; }
|
.ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; }
|
||||||
.ai-message.ai { background: var(--bg-card); border-left: 3px solid var(--accent); }
|
.ai-message.ai { background: var(--bg-card); border-left: 3px solid var(--accent); }
|
||||||
.ai-message.user { background: var(--accent-bg); border-left: 3px solid var(--accent-muted); }
|
.ai-message.user { background: var(--accent-bg); border-left: 3px solid var(--accent-muted); }
|
||||||
|
.ai-message.thinking { background: var(--bg-elevated); border-left: 3px solid var(--info); font-style: italic; color: var(--text-tertiary); }
|
||||||
|
.ai-message.tool { background: var(--bg-elevated); border-left: 3px solid var(--warning); }
|
||||||
|
.ai-message.tool .tool-name { font-weight: 700; color: var(--warning); }
|
||||||
|
.ai-message.tool .tool-args { font-family: var(--font-mono); font-size: 12px; color: var(--text-tertiary); margin-top: 4px; }
|
||||||
.ai-panel-input { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--border); }
|
.ai-panel-input { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--border); }
|
||||||
.ai-panel-input input { flex: 1; font-size: 13px; padding: 6px 10px; }
|
.ai-panel-input input { flex: 1; font-size: 13px; padding: 6px 10px; }
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
|||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:8095',
|
target: 'http://127.0.0.1:8095',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user