refactor: unify into single muyue binary with embedded desktop mode
All checks were successful
Beta Release / beta (push) Successful in 37s
All checks were successful
Beta Release / beta (push) Successful in 37s
- Merge muyue + muyue-desktop into one binary (13MB) - `muyue` starts TUI, `muyue desktop` launches web UI in browser - Move frontend from cmd/muyue-desktop/frontend/ to web/ (standard Go layout) - Add web/embed.go with //go:embed all:dist for frontend assets - Add internal/desktop/ package (server, browser open, SPA routing, signals) - Split internal/api/api.go into server.go + handlers.go - Add internal/desktop/desktop.go with SPA fallback and --port/--no-open flags - Clean package.json: remove unused @xterm/xterm, switch to ESM - Fix vite.config.js proxy to use port 8095 for dev mode - Add Makefile targets: frontend, desktop, dev-desktop - Update all CI workflows: single binary build, web/ paths - Remove cmd/muyue-desktop/ entirely 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user