Compare commits
16 Commits
v0.3.5-bet
...
v0.3.5-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
399b845e14 | ||
|
|
436d5c6149 | ||
|
|
5a9edc076e | ||
|
|
5bdc7a6429 | ||
|
|
5a0480bae0 | ||
|
|
80de4dd523 | ||
|
|
de52f4ebd6 | ||
|
|
98ff0dd578 | ||
|
|
9a1ff6e8dc | ||
|
|
034b9ee0e4 | ||
|
|
c1b1fc653f | ||
|
|
50ca75180c | ||
|
|
b8aa935bec | ||
|
|
5627ddd2ce | ||
|
|
d27872572a | ||
|
|
7d0f807fb0 |
@@ -530,6 +530,11 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "mimo":
|
||||||
|
q.Healthy = p.APIKey != ""
|
||||||
|
if p.APIKey == "" {
|
||||||
|
q.Error = "no API key"
|
||||||
|
}
|
||||||
case "claude", "anthropic":
|
case "claude", "anthropic":
|
||||||
// Claude Code n'a pas d'API externe, vérifier l'installation
|
// Claude Code n'a pas d'API externe, vérifier l'installation
|
||||||
claudePath := "/usr/bin/claude"
|
claudePath := "/usr/bin/claude"
|
||||||
|
|||||||
@@ -269,6 +269,12 @@ func Default() *MuyueConfig {
|
|||||||
BaseURL: "https://api.minimax.io/v1",
|
BaseURL: "https://api.minimax.io/v1",
|
||||||
Active: true,
|
Active: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "mimo",
|
||||||
|
Model: "MiMo-V2.5-Pro",
|
||||||
|
BaseURL: "https://token-plan-ams.xiaomimimo.com/v1",
|
||||||
|
Active: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "zai",
|
Name: "zai",
|
||||||
Model: "glm",
|
Model: "glm",
|
||||||
|
|||||||
@@ -476,6 +476,8 @@ func getProviderBaseURL(name string) string {
|
|||||||
return "https://api.openai.com/v1"
|
return "https://api.openai.com/v1"
|
||||||
case "zai":
|
case "zai":
|
||||||
return "https://api.z.ai/v1"
|
return "https://api.z.ai/v1"
|
||||||
|
case "mimo":
|
||||||
|
return "https://token-plan-ams.xiaomimimo.com/v1"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -503,11 +505,19 @@ func (o *Orchestrator) sendWithFallback(reqBody ChatRequest, baseURLOverride str
|
|||||||
if o.provider != nil {
|
if o.provider != nil {
|
||||||
providerOrder = append(providerOrder, o.provider)
|
providerOrder = append(providerOrder, o.provider)
|
||||||
}
|
}
|
||||||
|
var zaiProvider *config.AIProvider
|
||||||
for _, p := range providers {
|
for _, p := range providers {
|
||||||
if o.provider == nil || p.Name != o.provider.Name {
|
if o.provider == nil || p.Name != o.provider.Name {
|
||||||
providerOrder = append(providerOrder, p)
|
if p.Name == "zai" {
|
||||||
|
zaiProvider = p
|
||||||
|
} else {
|
||||||
|
providerOrder = append(providerOrder, p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if zaiProvider != nil {
|
||||||
|
providerOrder = append(providerOrder, zaiProvider)
|
||||||
|
}
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
var triedProviders []string
|
var triedProviders []string
|
||||||
|
|||||||
1
web/.npmrc
Normal file
1
web/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
legacy-peer-deps=true
|
||||||
48
web/package-lock.json
generated
48
web/package-lock.json
generated
@@ -7,8 +7,12 @@
|
|||||||
"name": "muyue-web",
|
"name": "muyue-web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-image": "^0.10.0-beta.203",
|
||||||
|
"@xterm/addon-search": "^0.17.0-beta.203",
|
||||||
|
"@xterm/addon-unicode11": "^0.10.0-beta.203",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/addon-webgl": "^0.20.0-beta.202",
|
||||||
|
"@xterm/xterm": "^6.1.0-beta.203",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5"
|
||||||
@@ -406,16 +410,52 @@
|
|||||||
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
|
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-image": {
|
||||||
|
"version": "0.10.0-beta.203",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.203.tgz",
|
||||||
|
"integrity": "sha512-1hRy7/jYCYvUhc6GYu177EdsW44QQQHsq71Odvo6cEhHKEEoqFsrOnLpe9WuNWZXgqpCwy2Cnp6FepHm960Eiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^6.1.0-beta.203"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/addon-search": {
|
||||||
|
"version": "0.17.0-beta.203",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.203.tgz",
|
||||||
|
"integrity": "sha512-agxzh30h4L82kjGlTwWEsaXnXzOuMIAm80+zcNElFL/hHuT/nLvcwRng+s7RzOWNNLG3pB4jbTHqbBaM+nW8mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^6.1.0-beta.203"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/addon-unicode11": {
|
||||||
|
"version": "0.10.0-beta.203",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.203.tgz",
|
||||||
|
"integrity": "sha512-KqMOqqpeEPQw5TQLb8jNHPESjZSwenFzhBPNA1g2zcPY5JtZ15pFzzoFxXdzS5LYmdYxexpd8s2ianf8WmQKyg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^6.1.0-beta.203"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xterm/addon-web-links": {
|
"node_modules/@xterm/addon-web-links": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
|
||||||
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
|
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-webgl": {
|
||||||
|
"version": "0.20.0-beta.202",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.202.tgz",
|
||||||
|
"integrity": "sha512-GCh0QlUv77XX8cJt8/7AVdDUNFpa1f6MGX/skhciu5ZRK88hR1m8T+8MZ3FYfddLV6phY0ksmiO9ErC0R+7G/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^6.1.0-beta.203"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xterm/xterm": {
|
"node_modules/@xterm/xterm": {
|
||||||
"version": "6.0.0",
|
"version": "6.1.0-beta.203",
|
||||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.203.tgz",
|
||||||
"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
|
"integrity": "sha512-Ctqf05M6fPWZkfKxC4hy2+PP5P2BlVnJLbIsXZMpkCz/MjJvcf5OwwsGkq+nzhFDuojSX+rc2RxIetLONUBGqw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"addons/*"
|
"addons/*"
|
||||||
|
|||||||
@@ -9,8 +9,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-image": "^0.10.0-beta.203",
|
||||||
|
"@xterm/addon-search": "^0.17.0-beta.203",
|
||||||
|
"@xterm/addon-unicode11": "^0.10.0-beta.203",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/addon-webgl": "^0.20.0-beta.202",
|
||||||
|
"@xterm/xterm": "^6.1.0-beta.203",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5"
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export default function App() {
|
|||||||
shell: [
|
shell: [
|
||||||
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') },
|
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') },
|
||||||
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') },
|
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') },
|
||||||
|
{ keys: `${layout.keys.ctrl}+F`, desc: t('statusbar.search') },
|
||||||
|
{ keys: `${layout.keys.ctrl}+/Ctrl−`, desc: t('statusbar.zoom') },
|
||||||
|
{ keys: `Alt+1-7`, desc: t('statusbar.switchTab') },
|
||||||
|
{ keys: `${layout.keys.shift}+Tab`, desc: t('statusbar.nextTab') },
|
||||||
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
||||||
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
|||||||
setValidating(null)
|
setValidating(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayed = providers.filter(p => p.name === 'minimax' || p.name === 'zai')
|
const displayed = providers.filter(p => p.name === 'minimax' || p.name === 'mimo')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-providers-list">
|
<div className="config-providers-list">
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default function Dashboard({ api, refreshRef }) {
|
|||||||
}, [loadData, refreshRef])
|
}, [loadData, refreshRef])
|
||||||
|
|
||||||
const minimax = (quota || []).find(p => p.name === 'minimax')
|
const minimax = (quota || []).find(p => p.name === 'minimax')
|
||||||
const zai = (quota || []).find(p => p.name === 'zai')
|
const mimo = (quota || []).find(p => p.name === 'mimo')
|
||||||
|
|
||||||
const EXCLUDE_CMDS = ['ls', 'cd', 'pwd', 'clear', 'exit', 'history', 'cat', 'echo', 'grep', 'export', 'alias', 'unalias', 'set', 'unset', 'source', '.', 'fg', 'bg', 'jobs', 'wait', 'true', 'false', 'yes', 'sleep', 'date', 'whoami', 'id', 'uname', 'hostname', 'uptime', 'df', 'free', 'top', 'htop', 'nano', 'vi', 'vim', 'less', 'more', 'tail', 'head', 'man', 'info', 'which', 'whereis', 'type', 'command', 'hash', 'builtin', 'help']
|
const EXCLUDE_CMDS = ['ls', 'cd', 'pwd', 'clear', 'exit', 'history', 'cat', 'echo', 'grep', 'export', 'alias', 'unalias', 'set', 'unset', 'source', '.', 'fg', 'bg', 'jobs', 'wait', 'true', 'false', 'yes', 'sleep', 'date', 'whoami', 'id', 'uname', 'hostname', 'uptime', 'df', 'free', 'top', 'htop', 'nano', 'vi', 'vim', 'less', 'more', 'tail', 'head', 'man', 'info', 'which', 'whereis', 'type', 'command', 'hash', 'builtin', 'help']
|
||||||
|
|
||||||
@@ -186,22 +186,22 @@ export default function Dashboard({ api, refreshRef }) {
|
|||||||
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{minimax.error || 'no data'}</span>
|
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{minimax.error || 'no data'}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{zai && zai.data?.models?.map((m, i) => (
|
{mimo && mimo.data?.models?.map((m, i) => (
|
||||||
<div key={i} className="dash-quota-row">
|
<div key={i} className="dash-quota-row">
|
||||||
<span className="dash-quota-name">{String(m.model)}</span>
|
<span className="dash-quota-name">{String(m.model).replace('MiMo-', '')}</span>
|
||||||
<div className="dash-bar">
|
<div className="dash-bar">
|
||||||
<div className="dash-bar-fill" style={{ width: `${Math.min(100, (m.used / m.total) * 100)}%` }} />
|
<div className="dash-bar-fill" style={{ width: `${Math.min(100, (m.used / m.total) * 100)}%` }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="dash-quota-val">{m.used}/{m.total}</span>
|
<span className="dash-quota-val">{m.used}/{m.total}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{zai && !zai.data?.models?.length && (
|
{mimo && !mimo.data?.models?.length && (
|
||||||
<div className="dash-quota-row">
|
<div className="dash-quota-row">
|
||||||
<span className="dash-quota-name">Z.AI</span>
|
<span className="dash-quota-name">MiMo</span>
|
||||||
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{zai.error || 'no data'}</span>
|
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{mimo.error || (mimo.healthy ? '✓ configured' : 'no key')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!minimax && !zai && <span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>No providers</span>}
|
{!minimax && !mimo && <span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>No providers</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { Terminal as XTerm } from '@xterm/xterm'
|
import { Terminal as XTerm } from '@xterm/xterm'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||||
import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye } from 'lucide-react'
|
import { WebglAddon } from '@xterm/addon-webgl'
|
||||||
|
import { SearchAddon } from '@xterm/addon-search'
|
||||||
|
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||||
|
import { ImageAddon } from '@xterm/addon-image'
|
||||||
|
import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot } from 'lucide-react'
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '../i18n'
|
||||||
|
|
||||||
|
const AI_TAB_ID = 0
|
||||||
const MAX_TABS = 7
|
const MAX_TABS = 7
|
||||||
const SHELL_MAX_TOKENS = 100000
|
const SHELL_MAX_TOKENS = 100000
|
||||||
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
||||||
@@ -67,7 +72,70 @@ function formatText(text) {
|
|||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Style thème système pour xterm ===
|
||||||
|
function getCSSVariable(varName) {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHexColor(hex) {
|
||||||
|
if (!hex || hex.startsWith('var(')) return null;
|
||||||
|
hex = hex.replace('#', '');
|
||||||
|
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
||||||
|
if (hex.length !== 6) return null;
|
||||||
|
const r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRgbString(hex) {
|
||||||
|
const c = parseHexColor(hex);
|
||||||
|
if (!c) return '#000000';
|
||||||
|
return `#${c.r.toString(16).padStart(2, '0')}${c.g.toString(16).padStart(2, '0')}${c.b.toString(16).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSystemTheme() {
|
||||||
|
const bg = getCSSVariable('--bg-base') || '#0F0D10';
|
||||||
|
const fg = getCSSVariable('--text-primary') || '#EAE0E2';
|
||||||
|
const accent = getCSSVariable('--accent-light') || '#FF1A5E';
|
||||||
|
const accentDim = getCSSVariable('--accent-dim') || '#6B2033';
|
||||||
|
const success = '#00E676';
|
||||||
|
const warning = '#FFD740';
|
||||||
|
const error = getCSSVariable('--accent-bright') || '#FF1744';
|
||||||
|
const bgSurface = getCSSVariable('--bg-surface') || bg;
|
||||||
|
const bgElevated = getCSSVariable('--bg-elevated') || bgSurface;
|
||||||
|
const textSecondary = getCSSVariable('--text-secondary') || fg;
|
||||||
|
const textTertiary = getCSSVariable('--text-tertiary') || textSecondary;
|
||||||
|
|
||||||
|
return {
|
||||||
|
background: toRgbString(bg),
|
||||||
|
foreground: toRgbString(fg),
|
||||||
|
cursor: toRgbString(accent),
|
||||||
|
cursorAccent: toRgbString(bg),
|
||||||
|
selectionBackground: `${toRgbString(accentDim)}44`,
|
||||||
|
selectionForeground: '#FFFFFF',
|
||||||
|
black: toRgbString(bgElevated),
|
||||||
|
red: toRgbString(error),
|
||||||
|
green: toRgbString(success),
|
||||||
|
yellow: toRgbString(warning),
|
||||||
|
blue: toRgbString(getCSSVariable('--accent') || '#448AFF'),
|
||||||
|
magenta: toRgbString(accent),
|
||||||
|
cyan: '#00BCD4',
|
||||||
|
white: toRgbString(fg),
|
||||||
|
brightBlack: toRgbString(bgSurface),
|
||||||
|
brightRed: toRgbString(accent),
|
||||||
|
brightGreen: toRgbString(success),
|
||||||
|
brightYellow: toRgbString(warning),
|
||||||
|
brightBlue: toRgbString(getCSSVariable('--accent-muted') || '#82B1FF'),
|
||||||
|
brightMagenta: toRgbString(getCSSVariable('--accent-soft') || '#FF80AB'),
|
||||||
|
brightCyan: '#84FFFF',
|
||||||
|
brightWhite: '#FFFFFF',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const THEMES = {
|
const THEMES = {
|
||||||
|
system: buildSystemTheme(),
|
||||||
default: {
|
default: {
|
||||||
background: '#0A0A0C', foreground: '#EAE0E2', cursor: '#FF0033',
|
background: '#0A0A0C', foreground: '#EAE0E2', cursor: '#FF0033',
|
||||||
cursorAccent: '#0A0A0C', selectionBackground: '#FF003344', selectionForeground: '#ffffff',
|
cursorAccent: '#0A0A0C', selectionBackground: '#FF003344', selectionForeground: '#ffffff',
|
||||||
@@ -125,13 +193,17 @@ const THEMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTheme(themeName) {
|
function getTheme(themeName) {
|
||||||
return THEMES[themeName] || THEMES.default
|
if (themeName === 'system' || themeName === 'default') {
|
||||||
|
return buildSystemTheme()
|
||||||
|
}
|
||||||
|
return THEMES[themeName] || buildSystemTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTerminal(container, settings = {}) {
|
function createTerminal(container, settings = {}) {
|
||||||
const theme = getTheme(settings.theme || 'default')
|
const theme = getTheme(settings.theme || 'system')
|
||||||
const term = new XTerm({
|
const term = new XTerm({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
|
allowProposedApi: true,
|
||||||
fontSize: settings.fontSize || 12,
|
fontSize: settings.fontSize || 12,
|
||||||
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||||
theme,
|
theme,
|
||||||
@@ -141,8 +213,25 @@ function createTerminal(container, settings = {}) {
|
|||||||
|
|
||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon()
|
||||||
const webLinksAddon = new WebLinksAddon()
|
const webLinksAddon = new WebLinksAddon()
|
||||||
|
const searchAddon = new SearchAddon()
|
||||||
|
const unicode11Addon = new Unicode11Addon()
|
||||||
|
const imageAddon = new ImageAddon()
|
||||||
|
|
||||||
term.loadAddon(fitAddon)
|
term.loadAddon(fitAddon)
|
||||||
term.loadAddon(webLinksAddon)
|
term.loadAddon(webLinksAddon)
|
||||||
|
term.loadAddon(searchAddon)
|
||||||
|
term.loadAddon(unicode11Addon)
|
||||||
|
term.loadAddon(imageAddon)
|
||||||
|
|
||||||
|
term.unicode.activeVersion = '11'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const webglAddon = new WebglAddon()
|
||||||
|
webglAddon.onContextLoss(() => { webglAddon.dispose() })
|
||||||
|
term.loadAddon(webglAddon)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Shell] WebGL renderer not available, using DOM fallback:', e)
|
||||||
|
}
|
||||||
|
|
||||||
term.attachCustomKeyEventHandler((e) => {
|
term.attachCustomKeyEventHandler((e) => {
|
||||||
if (e.type !== 'keydown') return true
|
if (e.type !== 'keydown') return true
|
||||||
@@ -166,13 +255,34 @@ function createTerminal(container, settings = {}) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctrl && (e.key === '=' || e.key === '+')) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
window.dispatchEvent(new CustomEvent('shell-zoom', { detail: 1 }))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctrl && e.key === '-') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
window.dispatchEvent(new CustomEvent('shell-zoom', { detail: -1 }))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctrl && e.key === '0') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
window.dispatchEvent(new CustomEvent('shell-zoom', { detail: 0 }))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
term.open(container)
|
term.open(container)
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
|
|
||||||
return { term, fitAddon }
|
return { term, fitAddon, searchAddon }
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectWebSocket(term, fitAddon, initPayload, onStateChange, onFirstMessage) {
|
function connectWebSocket(term, fitAddon, initPayload, onStateChange, onFirstMessage) {
|
||||||
@@ -182,9 +292,20 @@ function connectWebSocket(term, fitAddon, initPayload, onStateChange, onFirstMes
|
|||||||
ws.addEventListener('open', () => {
|
ws.addEventListener('open', () => {
|
||||||
ws.send(JSON.stringify(initPayload))
|
ws.send(JSON.stringify(initPayload))
|
||||||
const dims = fitAddon.proposeDimensions()
|
const dims = fitAddon.proposeDimensions()
|
||||||
if (dims) {
|
// Envoyer resize avec dimensions minimales garanties (24x80)
|
||||||
ws.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols }))
|
const rows = dims?.rows || 24
|
||||||
}
|
const cols = dims?.cols || 80
|
||||||
|
ws.send(JSON.stringify({ type: 'resize', rows, cols }))
|
||||||
|
// Forcer un fit après l'ouverture
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
fitAddon.fit()
|
||||||
|
const newDims = fitAddon.proposeDimensions()
|
||||||
|
if (newDims && newDims.rows > 0 && newDims.cols > 0) {
|
||||||
|
ws.send(JSON.stringify({ type: 'resize', rows: newDims.rows, cols: newDims.cols }))
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('[Shell] fit failed:', e) }
|
||||||
|
}, 50)
|
||||||
if (onStateChange) onStateChange(true)
|
if (onStateChange) onStateChange(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -229,29 +350,44 @@ export default function Shell({ api }) {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const tabsRef = useRef({})
|
const tabsRef = useRef({})
|
||||||
const nextIdRef = useRef(1)
|
const nextIdRef = useRef(1)
|
||||||
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
|
const settingsRef = useRef({ fontSize: 10, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'system' })
|
||||||
const pendingCommandsRef = useRef({})
|
const pendingCommandsRef = useRef({})
|
||||||
|
|
||||||
const savedTabs = (() => {
|
const [tabs, setTabs] = useState(() => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(TABS_STORAGE_KEY)
|
const raw = localStorage.getItem(TABS_STORAGE_KEY)
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = JSON.parse(raw)
|
||||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
if (Array.isArray(parsed) && parsed.length > 0 && parsed.length <= MAX_TABS) {
|
||||||
return parsed.map(t => ({ ...t, connected: false }))
|
return parsed.map((t, i) => ({
|
||||||
|
id: t.id || i + 1,
|
||||||
|
name: t.name || `Tab ${i + 1}`,
|
||||||
|
type: t.type || 'local',
|
||||||
|
shell: t.shell || '',
|
||||||
|
host: t.host,
|
||||||
|
port: t.port,
|
||||||
|
user: t.user,
|
||||||
|
key_path: t.key_path,
|
||||||
|
connected: false
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (e) {
|
||||||
return null
|
console.warn('[Shell] Failed to parse saved tabs:', e)
|
||||||
})()
|
localStorage.removeItem(TABS_STORAGE_KEY)
|
||||||
|
|
||||||
const [tabs, setTabs] = useState(savedTabs || [
|
|
||||||
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
|
|
||||||
])
|
|
||||||
const [activeTab, setActiveTab] = useState(() => {
|
|
||||||
if (savedTabs) {
|
|
||||||
return savedTabs[0]?.id || 1
|
|
||||||
}
|
}
|
||||||
|
return [
|
||||||
|
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const [activeTab, setActiveTab] = useState(() => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(TABS_STORAGE_KEY)
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) return parsed[0]?.id || 1
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
const activeTabRef = useRef(activeTab)
|
const activeTabRef = useRef(activeTab)
|
||||||
@@ -263,13 +399,45 @@ export default function Shell({ api }) {
|
|||||||
const [editingTab, setEditingTab] = useState(null)
|
const [editingTab, setEditingTab] = useState(null)
|
||||||
const [editName, setEditName] = useState('')
|
const [editName, setEditName] = useState('')
|
||||||
const [terminalSettings, setTerminalSettings] = useState({
|
const [terminalSettings, setTerminalSettings] = useState({
|
||||||
fontSize: 12,
|
fontSize: 10,
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||||
theme: 'default',
|
theme: 'system',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [showSearch, setShowSearch] = useState(false)
|
||||||
|
const [searchText, setSearchText] = useState('')
|
||||||
|
const searchInputRef = useRef(null)
|
||||||
|
const searchDecorationsRef = useRef(null)
|
||||||
|
const [zoomLevel, setZoomLevel] = useState(0)
|
||||||
|
const baseFontSizeRef = useRef(12)
|
||||||
|
|
||||||
useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
|
useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
baseFontSizeRef.current = terminalSettings.fontSize || 12
|
||||||
|
}, [terminalSettings.fontSize])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e) => {
|
||||||
|
const direction = e.detail
|
||||||
|
setZoomLevel(prev => {
|
||||||
|
let next
|
||||||
|
if (direction === 0) next = 0
|
||||||
|
else next = Math.max(-8, Math.min(10, prev + direction))
|
||||||
|
const newSize = baseFontSizeRef.current + next * 2
|
||||||
|
for (const entry of Object.values(tabsRef.current)) {
|
||||||
|
if (entry.term && !entry.term._disposed) {
|
||||||
|
entry.term.options.fontSize = newSize
|
||||||
|
try { entry.fitAddon.fit() } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
window.addEventListener('shell-zoom', handler)
|
||||||
|
return () => window.removeEventListener('shell-zoom', handler)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [sshForm, setSshForm] = useState({
|
const [sshForm, setSshForm] = useState({
|
||||||
name: '', host: '', port: 22, user: '', key_path: '',
|
name: '', host: '', port: 22, user: '', key_path: '',
|
||||||
})
|
})
|
||||||
@@ -328,9 +496,9 @@ export default function Shell({ api }) {
|
|||||||
api.getConfig().then(d => {
|
api.getConfig().then(d => {
|
||||||
if (d.terminal) {
|
if (d.terminal) {
|
||||||
setTerminalSettings({
|
setTerminalSettings({
|
||||||
fontSize: d.terminal.font_size || 12,
|
fontSize: d.terminal.font_size || 10,
|
||||||
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||||
theme: d.terminal.theme || 'default',
|
theme: d.terminal.theme || 'system',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
@@ -343,8 +511,9 @@ export default function Shell({ api }) {
|
|||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
const s = settingsRef.current
|
const s = settingsRef.current
|
||||||
const { term, fitAddon } = createTerminal(container, {
|
const effectiveFontSize = s.fontSize + zoomLevel * 2
|
||||||
fontSize: s.fontSize,
|
const { term, fitAddon, searchAddon } = createTerminal(container, {
|
||||||
|
fontSize: effectiveFontSize,
|
||||||
fontFamily: s.fontFamily,
|
fontFamily: s.fontFamily,
|
||||||
theme: s.theme,
|
theme: s.theme,
|
||||||
})
|
})
|
||||||
@@ -435,7 +604,7 @@ export default function Shell({ api }) {
|
|||||||
const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000)
|
const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000)
|
||||||
|
|
||||||
console.log(`[Shell] initTerminal tab=${tabId} type=${tab.type} name="${tab.name}" shell="${tab.shell || '(default)'}"`)
|
console.log(`[Shell] initTerminal tab=${tabId} type=${tab.type} name="${tab.name}" shell="${tab.shell || '(default)'}"`)
|
||||||
tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed }
|
tabsRef.current[tabId] = { term, fitAddon, searchAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed }
|
||||||
tabsRef.current[tabId]._markDisposed = () => { disposed = true }
|
tabsRef.current[tabId]._markDisposed = () => { disposed = true }
|
||||||
console.log(`[Shell] initTerminal tab=${tabId} done, tabsRef keys:`, Object.keys(tabsRef.current))
|
console.log(`[Shell] initTerminal tab=${tabId} done, tabsRef keys:`, Object.keys(tabsRef.current))
|
||||||
|
|
||||||
@@ -482,33 +651,63 @@ export default function Shell({ api }) {
|
|||||||
let cancelled = false
|
let cancelled = false
|
||||||
const pending = []
|
const pending = []
|
||||||
|
|
||||||
|
// Forcer le layout à se calculer
|
||||||
|
const forceLayout = () => {
|
||||||
|
const el = document.querySelector('.shell-terminal-col')
|
||||||
|
if (el) {
|
||||||
|
el.style.height = ''
|
||||||
|
el.style.minHeight = ''
|
||||||
|
// Forcer reflow
|
||||||
|
void el.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tryInitTab = (tab, attempt) => {
|
const tryInitTab = (tab, attempt) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
const shellCol = document.querySelector('.shell-terminal-col')
|
if (attempt > 20) {
|
||||||
if (!shellCol || shellCol.offsetParent === null) {
|
console.warn(`[Shell] max attempts reached for tab ${tab.id}`)
|
||||||
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 200))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceLayout()
|
||||||
|
const shellCol = document.querySelector('.shell-terminal-col')
|
||||||
|
if (!shellCol) {
|
||||||
|
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 150))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const container = document.getElementById(`terminal-${tab.id}`)
|
const container = document.getElementById(`terminal-${tab.id}`)
|
||||||
if (!container) {
|
if (!container) {
|
||||||
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
|
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (container.offsetHeight === 0) {
|
|
||||||
|
const rect = container.getBoundingClientRect()
|
||||||
|
if (rect.height < 10 || rect.width < 10) {
|
||||||
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
|
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tabsRef.current[tab.id]) {
|
if (!tabsRef.current[tab.id]) {
|
||||||
initTerminal(tab.id, tab)
|
initTerminal(tab.id, tab)
|
||||||
}
|
}
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (cancelled) return
|
// Multiple fit attempts avec délais croissants
|
||||||
const entry = tabsRef.current[tab.id]
|
const fitAttempts = [0, 50, 100, 200, 400]
|
||||||
if (entry) {
|
fitAttempts.forEach(delay => {
|
||||||
entry.fitAddon.fit()
|
setTimeout(() => {
|
||||||
setTimeout(() => { if (!cancelled) entry.fitAddon.fit() }, 100)
|
if (cancelled) return
|
||||||
}
|
const entry = tabsRef.current[tab.id]
|
||||||
|
if (entry && entry.fitAddon) {
|
||||||
|
try {
|
||||||
|
entry.fitAddon.fit()
|
||||||
|
} catch (e) { console.warn(`[Shell] fit attempt ${delay}ms failed:`, e) }
|
||||||
|
}
|
||||||
|
}, delay)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tab of tabs) {
|
||||||
if (!tabsRef.current[tab.id]) {
|
if (!tabsRef.current[tab.id]) {
|
||||||
tryInitTab(tab, 0)
|
tryInitTab(tab, 0)
|
||||||
}
|
}
|
||||||
@@ -571,6 +770,15 @@ export default function Shell({ api }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e) => {
|
const onKey = (e) => {
|
||||||
|
const ctrl = e.ctrlKey || e.metaKey
|
||||||
|
if (ctrl && e.shiftKey && e.key === 'F') {
|
||||||
|
const shellTab = document.querySelector('.shell-layout')
|
||||||
|
if (!shellTab || shellTab.closest('.tab-hidden')) return
|
||||||
|
e.preventDefault()
|
||||||
|
setShowSearch(prev => !prev)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
||||||
if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return
|
if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return
|
||||||
|
|
||||||
@@ -594,6 +802,49 @@ export default function Shell({ api }) {
|
|||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [tabs])
|
}, [tabs])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showSearch && searchInputRef.current) {
|
||||||
|
searchInputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [showSearch])
|
||||||
|
|
||||||
|
const handleSearchChange = useCallback((value) => {
|
||||||
|
setSearchText(value)
|
||||||
|
const entry = tabsRef.current[activeTabRef.current]
|
||||||
|
if (!entry?.searchAddon) return
|
||||||
|
if (!value) {
|
||||||
|
entry.searchAddon.clearDecorations()
|
||||||
|
entry.searchAddon.clearActiveDecoration()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
searchDecorationsRef.current = entry.searchAddon.findNext(value)
|
||||||
|
} catch {}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSearchNext = useCallback(() => {
|
||||||
|
const entry = tabsRef.current[activeTabRef.current]
|
||||||
|
if (!entry?.searchAddon || !searchText) return
|
||||||
|
try { entry.searchAddon.findNext(searchText) } catch {}
|
||||||
|
}, [searchText])
|
||||||
|
|
||||||
|
const handleSearchPrev = useCallback(() => {
|
||||||
|
const entry = tabsRef.current[activeTabRef.current]
|
||||||
|
if (!entry?.searchAddon || !searchText) return
|
||||||
|
try { entry.searchAddon.findPrevious(searchText) } catch {}
|
||||||
|
}, [searchText])
|
||||||
|
|
||||||
|
const handleCloseSearch = useCallback(() => {
|
||||||
|
setShowSearch(false)
|
||||||
|
setSearchText('')
|
||||||
|
const entry = tabsRef.current[activeTabRef.current]
|
||||||
|
if (entry?.searchAddon) {
|
||||||
|
entry.searchAddon.clearDecorations()
|
||||||
|
entry.searchAddon.clearActiveDecoration()
|
||||||
|
}
|
||||||
|
if (entry?.term) entry.term.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const addLocalTab = (shell, name) => {
|
const addLocalTab = (shell, name) => {
|
||||||
if (tabs.length >= MAX_TABS) return
|
if (tabs.length >= MAX_TABS) return
|
||||||
const id = nextIdRef.current++
|
const id = nextIdRef.current++
|
||||||
@@ -650,6 +901,19 @@ export default function Shell({ api }) {
|
|||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Redimensionner le nouveau tab actif
|
||||||
|
setTimeout(() => {
|
||||||
|
const newActiveTabId = next.length > 0 ? next[next.length - 1].id : null
|
||||||
|
if (newActiveTabId) {
|
||||||
|
const entry = tabsRef.current[newActiveTabId]
|
||||||
|
if (entry && entry.fitAddon) {
|
||||||
|
try {
|
||||||
|
entry.fitAddon.fit()
|
||||||
|
} catch (e) { console.warn('[Shell] fit after close failed:', e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startRename = (tabId, e) => {
|
const startRename = (tabId, e) => {
|
||||||
@@ -850,6 +1114,11 @@ export default function Shell({ api }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shell-tab-actions">
|
<div className="shell-tab-actions">
|
||||||
|
{zoomLevel !== 0 && (
|
||||||
|
<span className="shell-zoom-badge">
|
||||||
|
{zoomLevel > 0 ? '+' : ''}{zoomLevel > 0 ? zoomLevel * 2 : zoomLevel * 2}px
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{tabs.length < MAX_TABS && (
|
{tabs.length < MAX_TABS && (
|
||||||
<div className="shell-new-tab-wrapper">
|
<div className="shell-new-tab-wrapper">
|
||||||
<button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}>
|
<button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}>
|
||||||
@@ -910,6 +1179,26 @@ export default function Shell({ api }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shell-xterm-wrapper">
|
<div className="shell-xterm-wrapper">
|
||||||
|
{showSearch && (
|
||||||
|
<div className="shell-search-bar">
|
||||||
|
<Search size={14} className="shell-search-icon" />
|
||||||
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
|
className="shell-search-input"
|
||||||
|
value={searchText}
|
||||||
|
onChange={e => handleSearchChange(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') { e.shiftKey ? handleSearchPrev() : handleSearchNext() }
|
||||||
|
if (e.key === 'Escape') handleCloseSearch()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
/>
|
||||||
|
<button className="shell-search-nav" onClick={handleSearchPrev} title="Précédent (Shift+Entrée)">↑</button>
|
||||||
|
<button className="shell-search-nav" onClick={handleSearchNext} title="Suivant (Entrée)">↓</button>
|
||||||
|
<button className="shell-search-close" onClick={handleCloseSearch}><X size={14} /></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
|
|||||||
@@ -452,15 +452,15 @@ export default function Studio({ api }) {
|
|||||||
api.getProviders().then(data => {
|
api.getProviders().then(data => {
|
||||||
const providers = data.providers || []
|
const providers = data.providers || []
|
||||||
const minimax = providers.find(p => p.name.toUpperCase() === 'MINIMAX')
|
const minimax = providers.find(p => p.name.toUpperCase() === 'MINIMAX')
|
||||||
const zai = providers.find(p => p.name.toUpperCase() === 'ZAI')
|
const mimo = providers.find(p => p.name.toUpperCase() === 'MIMO')
|
||||||
if (!minimax || !zai) {
|
if (!minimax || !mimo) {
|
||||||
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'MiniMax et ZAI doivent être configurés pour utiliser `/model change`.', time: new Date().toISOString() }])
|
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'MiniMax et MiMo doivent être configurés pour utiliser `/model change`.', time: new Date().toISOString() }])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const active = providers.find(p => p.active)
|
const active = providers.find(p => p.active)
|
||||||
const activeName = active ? active.name.toUpperCase() : ''
|
const activeName = active ? active.name.toUpperCase() : ''
|
||||||
const switchTo = activeName === 'MINIMAX' ? 'ZAI' : 'MINIMAX'
|
const switchTo = activeName === 'MINIMAX' ? 'MIMO' : 'MINIMAX'
|
||||||
const target = switchTo === 'MINIMAX' ? minimax : zai
|
const target = switchTo === 'MINIMAX' ? minimax : mimo
|
||||||
api.saveProvider({ name: target.name, active: true }).then(() => {
|
api.saveProvider({ name: target.name, active: true }).then(() => {
|
||||||
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: `✓ Provider changé: **${target.name}** (${target.model})`, time: new Date().toISOString() }])
|
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: `✓ Provider changé: **${target.name}** (${target.model})`, time: new Date().toISOString() }])
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const en = {
|
|||||||
newLine: 'New line',
|
newLine: 'New line',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
paste: 'Paste',
|
paste: 'Paste',
|
||||||
|
search: 'Search',
|
||||||
|
zoom: 'Zoom +/−',
|
||||||
|
switchTab: 'Switch tab',
|
||||||
|
nextTab: 'Next tab',
|
||||||
runCommand: 'Run command',
|
runCommand: 'Run command',
|
||||||
commandHistory: 'Command history',
|
commandHistory: 'Command history',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const fr = {
|
|||||||
newLine: 'Nouvelle ligne',
|
newLine: 'Nouvelle ligne',
|
||||||
copy: 'Copier',
|
copy: 'Copier',
|
||||||
paste: 'Coller',
|
paste: 'Coller',
|
||||||
|
search: 'Rechercher',
|
||||||
|
zoom: 'Zoom +/\u2212',
|
||||||
|
switchTab: 'Changer d\u2019onglet',
|
||||||
|
nextTab: 'Onglet suivant',
|
||||||
runCommand: 'Ex\u00e9cuter',
|
runCommand: 'Ex\u00e9cuter',
|
||||||
commandHistory: 'Historique',
|
commandHistory: 'Historique',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -329,6 +329,14 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
|
|
||||||
.shell-tab-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
|
.shell-tab-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.shell-zoom-badge {
|
||||||
|
font-size: 10px; font-family: var(--font-mono); font-weight: 600;
|
||||||
|
color: var(--accent); background: var(--accent-bg);
|
||||||
|
padding: 2px 6px; border-radius: 3px;
|
||||||
|
border: 1px solid var(--accent-dim);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.shell-new-tab-wrapper { position: relative; }
|
.shell-new-tab-wrapper { position: relative; }
|
||||||
.shell-new-tab-btn {
|
.shell-new-tab-btn {
|
||||||
display: flex; align-items: center; gap: 2px;
|
display: flex; align-items: center; gap: 2px;
|
||||||
@@ -383,6 +391,36 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
.shell-menu-divider { height: 1px; background: var(--border); margin: 4px 6px; }
|
.shell-menu-divider { height: 1px; background: var(--border); margin: 4px 6px; }
|
||||||
|
|
||||||
.shell-xterm-wrapper { flex: 1; min-height: 0; background: var(--bg); overflow: hidden; position: relative; }
|
.shell-xterm-wrapper { flex: 1; min-height: 0; background: var(--bg); overflow: hidden; position: relative; }
|
||||||
|
|
||||||
|
.shell-search-bar {
|
||||||
|
position: absolute; top: 8px; right: 12px; z-index: 20;
|
||||||
|
display: flex; align-items: center; gap: 4px;
|
||||||
|
background: var(--bg-elevated); border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius); padding: 4px 6px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.shell-search-icon { color: var(--text-tertiary); flex-shrink: 0; }
|
||||||
|
.shell-search-input {
|
||||||
|
width: 200px; font-size: 12px; padding: 3px 6px; border-radius: 4px;
|
||||||
|
background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border);
|
||||||
|
font-family: var(--font-mono); outline: none;
|
||||||
|
}
|
||||||
|
.shell-search-input:focus { border-color: var(--accent); }
|
||||||
|
.shell-search-nav {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 24px; height: 24px; border-radius: 4px;
|
||||||
|
background: transparent; border: 1px solid var(--border);
|
||||||
|
color: var(--text-tertiary); cursor: pointer; font-size: 12px;
|
||||||
|
padding: 0; transition: all 0.1s;
|
||||||
|
}
|
||||||
|
.shell-search-nav:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--accent-dark); }
|
||||||
|
.shell-search-close {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 24px; height: 24px; border-radius: 4px;
|
||||||
|
background: transparent; border: none;
|
||||||
|
color: var(--text-disabled); cursor: pointer; padding: 0;
|
||||||
|
}
|
||||||
|
.shell-search-close:hover { color: var(--accent); }
|
||||||
.shell-xterm-instance {
|
.shell-xterm-instance {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -1058,3 +1096,76 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === XTerm Custom Styling === */
|
||||||
|
/* Styles for xterm.js integrated with Muyue theme */
|
||||||
|
.shell-xterm-instance .xterm {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-xterm-instance .xterm-viewport {
|
||||||
|
background-color: var(--bg-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-xterm-instance .xterm-screen {
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for xterm */
|
||||||
|
.shell-xterm-instance .xterm-viewport::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-xterm-instance .xterm-viewport::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-xterm-instance .xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-xterm-instance .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--accent-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection styling */
|
||||||
|
.shell-xterm-instance .xterm-selection {
|
||||||
|
background: var(--accent-dim) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus ring styling */
|
||||||
|
.shell-xterm-instance .xterm:focus .xterm-helper-text-container {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure consistent font rendering */
|
||||||
|
.shell-xterm-instance .xterm .xterm-char-measure-element {
|
||||||
|
font-family: var(--font-mono) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bell animation styling */
|
||||||
|
.shell-xterm-instance .xterm-bell {
|
||||||
|
animation: xterm-bell-flash 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes xterm-bell-flash {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cursor styling */
|
||||||
|
.shell-xterm-instance .xterm-cursor {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link styling for web links addon */
|
||||||
|
.shell-xterm-instance .xterm-link {
|
||||||
|
color: var(--accent-light) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-xterm-instance .xterm-link:hover {
|
||||||
|
color: var(--accent-muted) !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user