Some checks failed
Tests / unit-tests (push) Failing after 43s
- Dodat creack/pty za pseudo-terminal podršku - Claude CLI se pokreće u pravom PTY-ju (puni TUI, boje, Shift+Tab) - xterm.js u browseru renderuje terminal identično konzoli - WebSocket bridge: tastatura → PTY stdin, PTY stdout → terminal - Ring buffer (128KB) za replay pri reconnect-u - Automatski reconnect nakon 2 sekunde - PTY sesije žive nezavisno od browsera (60min idle timeout) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
5.7 KiB
HTML
173 lines
5.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Claude — {{.Project}}</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
background: #0d1117;
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.terminal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.4rem 0.75rem;
|
|
background: #161b22;
|
|
border-bottom: 1px solid #2a2a4a;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 0.8rem;
|
|
flex-shrink: 0;
|
|
}
|
|
.terminal-header .title {
|
|
color: #e94560;
|
|
font-weight: 600;
|
|
}
|
|
.terminal-header .controls {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
}
|
|
.terminal-header .status {
|
|
font-size: 0.7rem;
|
|
color: #6c6c80;
|
|
}
|
|
.terminal-header .status.connected { color: #4caf50; }
|
|
.terminal-header a {
|
|
color: #a0a0b0;
|
|
text-decoration: none;
|
|
font-size: 0.75rem;
|
|
}
|
|
.terminal-header a:hover { color: #e94560; }
|
|
#terminal-container {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
.xterm { height: 100%; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="terminal-header">
|
|
<span class="title">claude — {{.Project}}</span>
|
|
<div class="controls">
|
|
<span id="ws-status" class="status">Povezivanje...</span>
|
|
<a href="/projects">← Projekti</a>
|
|
</div>
|
|
</div>
|
|
<div id="terminal-container"></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
<script>
|
|
const statusEl = document.getElementById('ws-status');
|
|
const container = document.getElementById('terminal-container');
|
|
|
|
const term = new Terminal({
|
|
cursorBlink: true,
|
|
fontSize: 14,
|
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
|
|
theme: {
|
|
background: '#0d1117',
|
|
foreground: '#e0e0e0',
|
|
cursor: '#e94560',
|
|
cursorAccent: '#0d1117',
|
|
selectionBackground: 'rgba(233, 69, 96, 0.3)',
|
|
black: '#0d1117',
|
|
red: '#f44336',
|
|
green: '#4caf50',
|
|
yellow: '#ff9800',
|
|
blue: '#2196f3',
|
|
magenta: '#e94560',
|
|
cyan: '#00bcd4',
|
|
white: '#e0e0e0',
|
|
brightBlack: '#6c6c80',
|
|
brightRed: '#ff6b81',
|
|
brightGreen: '#66bb6a',
|
|
brightYellow: '#ffb74d',
|
|
brightBlue: '#64b5f6',
|
|
brightMagenta: '#ff6b81',
|
|
brightCyan: '#4dd0e1',
|
|
brightWhite: '#ffffff'
|
|
},
|
|
allowProposedApi: true,
|
|
scrollback: 10000,
|
|
convertEol: false,
|
|
});
|
|
|
|
const fitAddon = new FitAddon.FitAddon();
|
|
term.loadAddon(fitAddon);
|
|
term.loadAddon(new WebLinksAddon.WebLinksAddon());
|
|
|
|
term.open(container);
|
|
fitAddon.fit();
|
|
|
|
let ws;
|
|
let reconnectTimer;
|
|
|
|
function connect() {
|
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
ws = new WebSocket(`${proto}//${location.host}/ws?project={{.Project}}&project_dir={{.ProjectDir}}`);
|
|
ws.binaryType = 'arraybuffer';
|
|
|
|
ws.onopen = function() {
|
|
statusEl.textContent = 'Povezan';
|
|
statusEl.className = 'status connected';
|
|
// Send initial terminal size
|
|
ws.send(JSON.stringify({type: 'resize', cols: term.cols, rows: term.rows}));
|
|
term.focus();
|
|
};
|
|
|
|
ws.onmessage = function(event) {
|
|
if (event.data instanceof ArrayBuffer) {
|
|
term.write(new Uint8Array(event.data));
|
|
} else {
|
|
term.write(event.data);
|
|
}
|
|
};
|
|
|
|
ws.onclose = function() {
|
|
statusEl.textContent = 'Nepovezan';
|
|
statusEl.className = 'status';
|
|
// Auto-reconnect after 2 seconds
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = setTimeout(connect, 2000);
|
|
};
|
|
|
|
ws.onerror = function() {
|
|
statusEl.textContent = 'Greška';
|
|
statusEl.className = 'status';
|
|
};
|
|
}
|
|
|
|
// Send keyboard input to server
|
|
term.onData(function(data) {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(data);
|
|
}
|
|
});
|
|
|
|
// Send terminal resize to server
|
|
term.onResize(function(size) {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({type: 'resize', cols: size.cols, rows: size.rows}));
|
|
}
|
|
});
|
|
|
|
// Fit terminal on window resize
|
|
window.addEventListener('resize', function() {
|
|
fitAddon.fit();
|
|
});
|
|
|
|
// Start connection
|
|
connect();
|
|
</script>
|
|
</body>
|
|
</html>
|