claude-web-chat/templates/chat.html
djuka adea7ca28d
Some checks failed
Tests / unit-tests (push) Failing after 43s
Zamena chat UI sa pravim terminalom (xterm.js + PTY)
- 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>
2026-02-18 05:54:40 +00:00

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>