@bytespell/shella 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-plugins/agent/AGENT_SPEC.md +611 -0
- package/bundled-plugins/agent/README.md +7 -0
- package/bundled-plugins/agent/components.json +24 -0
- package/bundled-plugins/agent/eslint.config.js +23 -0
- package/bundled-plugins/agent/index.html +13 -0
- package/bundled-plugins/agent/package-lock.json +12140 -0
- package/bundled-plugins/agent/package.json +62 -0
- package/bundled-plugins/agent/public/vite.svg +1 -0
- package/bundled-plugins/agent/server.js +631 -0
- package/bundled-plugins/agent/src/App.tsx +755 -0
- package/bundled-plugins/agent/src/assets/react.svg +1 -0
- package/bundled-plugins/agent/src/components/ui/alert-dialog.tsx +182 -0
- package/bundled-plugins/agent/src/components/ui/badge.tsx +45 -0
- package/bundled-plugins/agent/src/components/ui/button.tsx +60 -0
- package/bundled-plugins/agent/src/components/ui/card.tsx +94 -0
- package/bundled-plugins/agent/src/components/ui/combobox.tsx +294 -0
- package/bundled-plugins/agent/src/components/ui/dropdown-menu.tsx +253 -0
- package/bundled-plugins/agent/src/components/ui/field.tsx +225 -0
- package/bundled-plugins/agent/src/components/ui/input-group.tsx +147 -0
- package/bundled-plugins/agent/src/components/ui/input.tsx +19 -0
- package/bundled-plugins/agent/src/components/ui/label.tsx +24 -0
- package/bundled-plugins/agent/src/components/ui/select.tsx +185 -0
- package/bundled-plugins/agent/src/components/ui/separator.tsx +26 -0
- package/bundled-plugins/agent/src/components/ui/switch.tsx +31 -0
- package/bundled-plugins/agent/src/components/ui/textarea.tsx +18 -0
- package/bundled-plugins/agent/src/index.css +131 -0
- package/bundled-plugins/agent/src/lib/utils.ts +6 -0
- package/bundled-plugins/agent/src/main.tsx +11 -0
- package/bundled-plugins/agent/src/reducer.test.ts +359 -0
- package/bundled-plugins/agent/src/reducer.ts +255 -0
- package/bundled-plugins/agent/src/store.ts +379 -0
- package/bundled-plugins/agent/src/types.ts +98 -0
- package/bundled-plugins/agent/src/utils.test.ts +393 -0
- package/bundled-plugins/agent/src/utils.ts +158 -0
- package/bundled-plugins/agent/tsconfig.app.json +32 -0
- package/bundled-plugins/agent/tsconfig.json +13 -0
- package/bundled-plugins/agent/tsconfig.node.json +26 -0
- package/bundled-plugins/agent/vite.config.ts +14 -0
- package/bundled-plugins/agent/vitest.config.ts +17 -0
- package/bundled-plugins/terminal/README.md +7 -0
- package/bundled-plugins/terminal/index.html +24 -0
- package/bundled-plugins/terminal/package-lock.json +3346 -0
- package/bundled-plugins/terminal/package.json +38 -0
- package/bundled-plugins/terminal/server.ts +265 -0
- package/bundled-plugins/terminal/src/App.tsx +153 -0
- package/bundled-plugins/terminal/src/TERMINAL_SPEC.md +404 -0
- package/bundled-plugins/terminal/src/main.tsx +9 -0
- package/bundled-plugins/terminal/src/store.ts +114 -0
- package/bundled-plugins/terminal/tsconfig.json +22 -0
- package/bundled-plugins/terminal/vite.config.ts +10 -0
- package/dist/src/plugin-manager.js +1 -1
- package/dist/src/plugin-manager.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shella/terminal",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.2.6",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.ts",
|
|
7
|
+
"shella": {
|
|
8
|
+
"displayName": "Terminal",
|
|
9
|
+
"devCommand": "tsx server.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "tsx server.ts",
|
|
13
|
+
"build": "vite build",
|
|
14
|
+
"preview": "vite preview"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
18
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
19
|
+
"@xterm/xterm": "^5.5.0",
|
|
20
|
+
"express": "^5.0.0",
|
|
21
|
+
"node-pty": "^1.2.0-beta.7",
|
|
22
|
+
"react": "^19.2.0",
|
|
23
|
+
"react-dom": "^19.2.0",
|
|
24
|
+
"ws": "^8.18.0",
|
|
25
|
+
"zustand": "^5.0.10"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/express": "^5.0.0",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"@types/react": "^19.2.0",
|
|
31
|
+
"@types/react-dom": "^19.2.0",
|
|
32
|
+
"@types/ws": "^8.5.0",
|
|
33
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
34
|
+
"tsx": "^4.19.0",
|
|
35
|
+
"typescript": "^5.6.0",
|
|
36
|
+
"vite": "^6.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { createServer } from 'http'
|
|
3
|
+
import { WebSocketServer, WebSocket } from 'ws'
|
|
4
|
+
import * as pty from 'node-pty'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const app = express()
|
|
11
|
+
const PORT = process.env.PORT
|
|
12
|
+
const INSTANCE_ID = process.env.INSTANCE_ID
|
|
13
|
+
const HOME = process.env.HOME || '/tmp'
|
|
14
|
+
|
|
15
|
+
// State persistence paths (following XDG spec)
|
|
16
|
+
const XDG_STATE_HOME = process.env.XDG_STATE_HOME || path.join(HOME, '.local', 'state')
|
|
17
|
+
const STATE_DIR = path.join(XDG_STATE_HOME, 'shella', 'plugins', 'terminal')
|
|
18
|
+
const STATE_FILE = INSTANCE_ID ? path.join(STATE_DIR, `${INSTANCE_ID}.json`) : null
|
|
19
|
+
|
|
20
|
+
interface PersistedState {
|
|
21
|
+
scrollback: string[]
|
|
22
|
+
cwd?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadPersistedState(): PersistedState | null {
|
|
26
|
+
if (!STATE_FILE) return null
|
|
27
|
+
try {
|
|
28
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
29
|
+
const content = fs.readFileSync(STATE_FILE, 'utf-8')
|
|
30
|
+
const state = JSON.parse(content) as PersistedState
|
|
31
|
+
console.log(`[terminal] Loaded persisted state for instance ${INSTANCE_ID}`)
|
|
32
|
+
return state
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('[terminal] Failed to load persisted state:', err)
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function savePersistedState(state: PersistedState): void {
|
|
41
|
+
if (!STATE_FILE) return
|
|
42
|
+
try {
|
|
43
|
+
fs.mkdirSync(STATE_DIR, { recursive: true })
|
|
44
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state), 'utf-8')
|
|
45
|
+
console.log(`[terminal] Saved state for instance ${INSTANCE_ID}`)
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('[terminal] Failed to save state:', err)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if we're in dev mode (no dist folder)
|
|
52
|
+
const isDev = !fs.existsSync(path.join(__dirname, 'dist', 'index.html'))
|
|
53
|
+
|
|
54
|
+
// Allow embedding in iframes (for shella-web)
|
|
55
|
+
app.use((_req, res, next) => {
|
|
56
|
+
res.removeHeader('X-Frame-Options')
|
|
57
|
+
res.setHeader('Content-Security-Policy', 'frame-ancestors *')
|
|
58
|
+
next()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
app.use(express.json())
|
|
62
|
+
|
|
63
|
+
// Health check
|
|
64
|
+
app.get('/api/health', (_req, res) => {
|
|
65
|
+
res.json({ status: 'ok', shell: process.env.SHELL || '/bin/bash' })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
async function startServer() {
|
|
69
|
+
if (isDev) {
|
|
70
|
+
// Dev mode: use Vite middleware for HMR
|
|
71
|
+
const { createServer: createViteServer } = await import('vite')
|
|
72
|
+
const vite = await createViteServer({
|
|
73
|
+
server: { middlewareMode: true, hmr: { port: Number(PORT) + 1000 } },
|
|
74
|
+
appType: 'spa',
|
|
75
|
+
})
|
|
76
|
+
app.use(vite.middlewares)
|
|
77
|
+
} else {
|
|
78
|
+
// Production: serve built files
|
|
79
|
+
app.use(express.static(path.join(__dirname, 'dist')))
|
|
80
|
+
app.get('/{*splat}', (_req, res) => {
|
|
81
|
+
res.sendFile(path.join(__dirname, 'dist', 'index.html'))
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const server = createServer(app)
|
|
86
|
+
|
|
87
|
+
// PTY is spawned lazily on first client connection with correct size
|
|
88
|
+
const shell = process.env.SHELL || '/bin/bash'
|
|
89
|
+
const home = process.env.HOME || '/tmp'
|
|
90
|
+
|
|
91
|
+
let ptyProcess: pty.IPty | null = null
|
|
92
|
+
|
|
93
|
+
// Track connected clients
|
|
94
|
+
const clients = new Set<WebSocket>()
|
|
95
|
+
|
|
96
|
+
// Load persisted state if available (for daemon recovery)
|
|
97
|
+
const persistedState = loadPersistedState()
|
|
98
|
+
|
|
99
|
+
// Buffer recent output for new clients (scrollback)
|
|
100
|
+
// Initialize with persisted scrollback if recovering
|
|
101
|
+
const outputBuffer: string[] = persistedState?.scrollback ?? []
|
|
102
|
+
const MAX_BUFFER_LINES = 1000
|
|
103
|
+
|
|
104
|
+
// Track if we have unsaved changes
|
|
105
|
+
let hasUnsavedChanges = false
|
|
106
|
+
|
|
107
|
+
// Chunk size for sending buffered output (256KB - well under iOS's 1MB WebSocket limit)
|
|
108
|
+
const MAX_MESSAGE_SIZE = 256 * 1024
|
|
109
|
+
|
|
110
|
+
function sendChunked(ws: WebSocket, data: string) {
|
|
111
|
+
for (let i = 0; i < data.length; i += MAX_MESSAGE_SIZE) {
|
|
112
|
+
const chunk = data.slice(i, i + MAX_MESSAGE_SIZE)
|
|
113
|
+
ws.send(JSON.stringify({ type: 'output', data: chunk }))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Broadcast master resize to all connected clients
|
|
118
|
+
function broadcastMasterResize(cols: number, rows: number) {
|
|
119
|
+
const message = JSON.stringify({ type: 'master_resize', cols, rows })
|
|
120
|
+
for (const client of clients) {
|
|
121
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
122
|
+
client.send(message)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function spawnPty(cols: number, rows: number) {
|
|
128
|
+
if (ptyProcess) return // Already spawned
|
|
129
|
+
|
|
130
|
+
ptyProcess = pty.spawn(shell, [], {
|
|
131
|
+
name: 'xterm-256color',
|
|
132
|
+
cols,
|
|
133
|
+
rows,
|
|
134
|
+
cwd: home,
|
|
135
|
+
env: {
|
|
136
|
+
...process.env as Record<string, string>,
|
|
137
|
+
// Suppress zsh's partial line marker (%) on fresh terminal
|
|
138
|
+
PROMPT_EOL_MARK: '',
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
console.log(`[terminal] PTY spawned: pid=${ptyProcess.pid}, shell=${shell}, size=${cols}x${rows}`)
|
|
143
|
+
|
|
144
|
+
// PTY -> all connected clients
|
|
145
|
+
ptyProcess.onData((data: string) => {
|
|
146
|
+
// Buffer output for new clients
|
|
147
|
+
outputBuffer.push(data)
|
|
148
|
+
if (outputBuffer.length > MAX_BUFFER_LINES) {
|
|
149
|
+
outputBuffer.shift()
|
|
150
|
+
}
|
|
151
|
+
hasUnsavedChanges = true
|
|
152
|
+
|
|
153
|
+
// Broadcast to all connected clients
|
|
154
|
+
const message = JSON.stringify({ type: 'output', data })
|
|
155
|
+
for (const client of clients) {
|
|
156
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
157
|
+
client.send(message)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
163
|
+
console.log(`[terminal] PTY exited: code=${exitCode}, signal=${signal}`)
|
|
164
|
+
const message = JSON.stringify({ type: 'exit', exitCode, signal })
|
|
165
|
+
for (const client of clients) {
|
|
166
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
167
|
+
client.send(message)
|
|
168
|
+
client.close()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// PTY died, shut down the instance
|
|
172
|
+
process.exit(exitCode ?? 0)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// WebSocket server for client connections
|
|
177
|
+
const wss = new WebSocketServer({ server, path: '/ws' })
|
|
178
|
+
|
|
179
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
180
|
+
clients.add(ws)
|
|
181
|
+
|
|
182
|
+
// Send current master size if PTY exists
|
|
183
|
+
if (ptyProcess) {
|
|
184
|
+
ws.send(JSON.stringify({
|
|
185
|
+
type: 'master_resize',
|
|
186
|
+
cols: ptyProcess.cols,
|
|
187
|
+
rows: ptyProcess.rows
|
|
188
|
+
}))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Send buffered output to new client so they see history
|
|
192
|
+
// This works for both active PTY output AND recovered scrollback from previous session
|
|
193
|
+
// Use chunked sending to avoid exceeding iOS's 1MB WebSocket message limit
|
|
194
|
+
if (outputBuffer.length > 0) {
|
|
195
|
+
sendChunked(ws, outputBuffer.join(''))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Client -> PTY
|
|
199
|
+
ws.on('message', (message: Buffer) => {
|
|
200
|
+
try {
|
|
201
|
+
const msg = JSON.parse(message.toString())
|
|
202
|
+
|
|
203
|
+
switch (msg.type) {
|
|
204
|
+
case 'input':
|
|
205
|
+
// Only forward input - no resize from input messages
|
|
206
|
+
ptyProcess?.write(msg.data)
|
|
207
|
+
break
|
|
208
|
+
case 'resize':
|
|
209
|
+
if (msg.cols && msg.rows) {
|
|
210
|
+
if (!ptyProcess) {
|
|
211
|
+
// First resize spawns the PTY with correct size
|
|
212
|
+
spawnPty(msg.cols, msg.rows)
|
|
213
|
+
// Broadcast the initial master size to all clients (including the one that just connected)
|
|
214
|
+
broadcastMasterResize(msg.cols, msg.rows)
|
|
215
|
+
} else {
|
|
216
|
+
ptyProcess.resize(msg.cols, msg.rows)
|
|
217
|
+
// Broadcast the new master size to all clients
|
|
218
|
+
broadcastMasterResize(msg.cols, msg.rows)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error('[terminal] Invalid message:', err)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
ws.on('close', () => {
|
|
229
|
+
clients.delete(ws)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
ws.on('error', (err) => {
|
|
233
|
+
console.error('[terminal] WebSocket error:', err)
|
|
234
|
+
clients.delete(ws)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
server.listen(PORT, () => {
|
|
239
|
+
console.log(`[terminal] Running on port ${PORT}${isDev ? ' (dev mode with HMR)' : ''}`)
|
|
240
|
+
if (persistedState) {
|
|
241
|
+
console.log(`[terminal] Restored ${persistedState.scrollback.length} lines of scrollback`)
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Periodic save (every 30 seconds if there are changes)
|
|
246
|
+
const saveInterval = setInterval(() => {
|
|
247
|
+
if (hasUnsavedChanges) {
|
|
248
|
+
savePersistedState({ scrollback: outputBuffer })
|
|
249
|
+
hasUnsavedChanges = false
|
|
250
|
+
}
|
|
251
|
+
}, 30000)
|
|
252
|
+
|
|
253
|
+
// Graceful shutdown - save state and kill PTY when instance stops
|
|
254
|
+
process.on('SIGTERM', () => {
|
|
255
|
+
console.log('[terminal] SIGTERM received, shutting down')
|
|
256
|
+
clearInterval(saveInterval)
|
|
257
|
+
// Save final state before exiting
|
|
258
|
+
savePersistedState({ scrollback: outputBuffer })
|
|
259
|
+
ptyProcess?.kill()
|
|
260
|
+
wss.clients.forEach((client) => client.close())
|
|
261
|
+
server.close(() => process.exit(0))
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
startServer()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react'
|
|
2
|
+
import { Terminal } from '@xterm/xterm'
|
|
3
|
+
import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
4
|
+
import { FitAddon } from '@xterm/addon-fit'
|
|
5
|
+
import '@xterm/xterm/css/xterm.css'
|
|
6
|
+
import { useTerminalStore } from './store'
|
|
7
|
+
|
|
8
|
+
export default function App() {
|
|
9
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
10
|
+
const terminalRef = useRef<HTMLDivElement>(null)
|
|
11
|
+
const fitAddonRef = useRef<FitAddon | null>(null)
|
|
12
|
+
const initialResizeSentRef = useRef(false)
|
|
13
|
+
|
|
14
|
+
const { status, connect, setTerminal, sendInput, sendResize } = useTerminalStore()
|
|
15
|
+
|
|
16
|
+
// Calculate optimal size for current viewport (used only for initial resize)
|
|
17
|
+
const calculateOptimalSize = useCallback(() => {
|
|
18
|
+
const container = containerRef.current
|
|
19
|
+
const terminal = useTerminalStore.getState().terminal
|
|
20
|
+
if (!container || !terminal) return null
|
|
21
|
+
|
|
22
|
+
const core = (terminal as unknown as { _core?: { _renderService?: { dimensions?: { css?: { cell?: { width: number; height: number } } } } } })._core
|
|
23
|
+
const cellDims = core?._renderService?.dimensions?.css?.cell
|
|
24
|
+
if (!cellDims) return null
|
|
25
|
+
|
|
26
|
+
const padding = 4
|
|
27
|
+
const availableWidth = container.clientWidth - padding * 2
|
|
28
|
+
const availableHeight = container.clientHeight - padding * 2
|
|
29
|
+
|
|
30
|
+
const cols = Math.floor(availableWidth / cellDims.width)
|
|
31
|
+
const rows = Math.floor(availableHeight / cellDims.height)
|
|
32
|
+
|
|
33
|
+
if (cols <= 0 || rows <= 0) return null
|
|
34
|
+
return { cols, rows }
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
// Send initial resize to spawn PTY (only once)
|
|
38
|
+
const sendInitialResize = useCallback(() => {
|
|
39
|
+
if (initialResizeSentRef.current) return
|
|
40
|
+
|
|
41
|
+
const optimal = calculateOptimalSize()
|
|
42
|
+
if (!optimal) return
|
|
43
|
+
|
|
44
|
+
initialResizeSentRef.current = true
|
|
45
|
+
sendResize(optimal.cols, optimal.rows)
|
|
46
|
+
}, [calculateOptimalSize, sendResize])
|
|
47
|
+
|
|
48
|
+
// Initialize terminal (once)
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!terminalRef.current) return
|
|
51
|
+
|
|
52
|
+
// Check if already initialized
|
|
53
|
+
const existingTerminal = useTerminalStore.getState().terminal
|
|
54
|
+
if (existingTerminal) return
|
|
55
|
+
|
|
56
|
+
const term = new Terminal({
|
|
57
|
+
cursorBlink: true,
|
|
58
|
+
fontSize: 14,
|
|
59
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
60
|
+
theme: {
|
|
61
|
+
background: '#1a1a1a',
|
|
62
|
+
foreground: '#e0e0e0',
|
|
63
|
+
cursor: '#e0e0e0',
|
|
64
|
+
cursorAccent: '#1a1a1a',
|
|
65
|
+
selectionBackground: '#4a4a4a',
|
|
66
|
+
black: '#1a1a1a',
|
|
67
|
+
red: '#ff5555',
|
|
68
|
+
green: '#50fa7b',
|
|
69
|
+
yellow: '#f1fa8c',
|
|
70
|
+
blue: '#6272a4',
|
|
71
|
+
magenta: '#ff79c6',
|
|
72
|
+
cyan: '#8be9fd',
|
|
73
|
+
white: '#e0e0e0',
|
|
74
|
+
brightBlack: '#4a4a4a',
|
|
75
|
+
brightRed: '#ff6e6e',
|
|
76
|
+
brightGreen: '#69ff94',
|
|
77
|
+
brightYellow: '#ffffa5',
|
|
78
|
+
brightBlue: '#d6acff',
|
|
79
|
+
brightMagenta: '#ff92df',
|
|
80
|
+
brightCyan: '#a4ffff',
|
|
81
|
+
brightWhite: '#ffffff',
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Add addons
|
|
86
|
+
const webLinksAddon = new WebLinksAddon()
|
|
87
|
+
const fitAddon = new FitAddon()
|
|
88
|
+
term.loadAddon(webLinksAddon)
|
|
89
|
+
term.loadAddon(fitAddon)
|
|
90
|
+
fitAddonRef.current = fitAddon
|
|
91
|
+
|
|
92
|
+
// Open terminal
|
|
93
|
+
term.open(terminalRef.current)
|
|
94
|
+
|
|
95
|
+
// Set up input handler
|
|
96
|
+
term.onData((data) => {
|
|
97
|
+
sendInput(data)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Store terminal and connect
|
|
101
|
+
setTerminal(term)
|
|
102
|
+
connect()
|
|
103
|
+
|
|
104
|
+
// Initial resize after a tick (to let xterm measure)
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
sendInitialResize()
|
|
107
|
+
}, 50)
|
|
108
|
+
|
|
109
|
+
// No cleanup - terminal persists in store
|
|
110
|
+
}, [connect, setTerminal, sendInput, sendInitialResize])
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
ref={containerRef}
|
|
115
|
+
style={{
|
|
116
|
+
width: '100%',
|
|
117
|
+
height: '100%',
|
|
118
|
+
backgroundColor: '#1a1a1a',
|
|
119
|
+
display: 'flex',
|
|
120
|
+
flexDirection: 'column',
|
|
121
|
+
overflow: 'hidden',
|
|
122
|
+
position: 'relative',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{status !== 'connected' && (
|
|
126
|
+
<div
|
|
127
|
+
style={{
|
|
128
|
+
padding: '8px 12px',
|
|
129
|
+
backgroundColor: status === 'error' ? '#ff5555' : '#4a4a4a',
|
|
130
|
+
color: '#fff',
|
|
131
|
+
fontSize: '12px',
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{status === 'connecting' && 'Connecting...'}
|
|
135
|
+
{status === 'disconnected' && 'Disconnected'}
|
|
136
|
+
{status === 'error' && 'Connection error'}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
<div
|
|
140
|
+
style={{
|
|
141
|
+
flex: 1,
|
|
142
|
+
display: 'flex',
|
|
143
|
+
alignItems: 'flex-start',
|
|
144
|
+
justifyContent: 'flex-start',
|
|
145
|
+
padding: '4px',
|
|
146
|
+
overflow: 'hidden',
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
<div ref={terminalRef} />
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|