Mode operatoire : session Claude Code persistante dans un navigateur

Stack ttyd + tmux + Caddy, reproduction d'une installation en production.
Inclut le frontend personnalise et les correctifs deja valides (FitAddon,
refit apres changement de police, double collage Ctrl+V, OSC 52, window.open).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code Bot
2026-07-27 07:28:02 +00:00
commit 830cb5d679
3 changed files with 1321 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
<style>
#ttyd-bar{position:fixed;top:0;left:0;right:0;height:30px;background:#111;display:flex;align-items:center;padding:0 6px;gap:4px;z-index:8000;border-bottom:1px solid #2a2a2a;}
#ttyd-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;white-space:nowrap;}
#ttyd-bar button:hover{background:#333;color:#fff;}
#ttyd-bar select{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 4px;font:11px monospace;cursor:pointer;}
#ttyd-conn{width:8px;height:8px;border-radius:50%;background:#555;margin:0 4px;flex-shrink:0;}
#ttyd-conn.ok{background:#4caf50;}
#ttyd-conn.err{background:#f44336;animation:blink 1s infinite;}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
#ttyd-spacer{flex:1;}
#ttyd-search-bar{position:fixed;top:30px;right:0;background:#1a1a1a;border:1px solid #333;border-top:none;border-right:none;border-radius:0 0 0 6px;display:flex;align-items:center;gap:4px;padding:4px 8px;z-index:8001;display:none;}
#ttyd-search-bar input{background:#111;color:#ddd;border:1px solid #333;border-radius:3px;padding:2px 6px;font:12px monospace;width:200px;outline:none;}
#ttyd-search-bar span{color:#666;font:11px monospace;min-width:60px;}
#ttyd-search-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;}
#terminal-container{position:absolute !important;top:30px;left:0;right:0;bottom:0;height:auto !important;padding-top:0 !important;}
#ttyd-ctx{position:fixed;background:#1e1e1e;border:1px solid #444;border-radius:4px;padding:4px 0;z-index:9999;min-width:150px;box-shadow:0 4px 12px rgba(0,0,0,.5);display:none;}
#ttyd-ctx div{padding:5px 14px;cursor:pointer;color:#ccc;font:12px monospace;}
#ttyd-ctx div:hover{background:#2d2d2d;color:#fff;}
#ttyd-ctx hr{border:none;border-top:1px solid #333;margin:3px 0;}
#ttyd-toast{position:fixed;bottom:8px;right:8px;background:rgba(20,20,20,.93);color:#7fff7f;font:12px monospace;padding:4px 10px;border-radius:4px;z-index:9999;pointer-events:none;opacity:0;transition:opacity .2s;}
#ttyd-toast.show{opacity:1;}
</style>
<div id="ttyd-bar">
<div id="ttyd-conn" title="Connexion WebSocket"></div>
<button onclick="ttydFontSize(-1)" title="Police -">A</button>
<span id="ttyd-fsize" style="color:#555;font:11px monospace;min-width:28px;text-align:center;">13</span>
<button onclick="ttydFontSize(+1)" title="Police +">A+</button>
<button onclick="ttydClear()" title="Vider">✕ Clear</button>
<button onclick="ttydSearch()" title="Rechercher (Ctrl+F)">⌕ Rech.</button>
<select id="ttyd-theme-sel" onchange="ttydSetTheme(this.value)" title="Thème"></select>
<button onclick="ttydSplit('h')" title="Split vertical (côte à côte)">⬜ ⬜</button>
<button onclick="ttydSplit('v')" title="Split horizontal (haut/bas)"><br style='line-height:3px'></button>
<button onclick="ttydPaneClose()" title="Fermer ce panneau">✕ Pan.</button>
<button onclick="ttydPaneNav('Left')" title="Panneau gauche"></button>
<button onclick="ttydPaneNav('Right')" title="Panneau droite"></button>
<button onclick="ttydPaneNav('Up')" title="Panneau haut"></button>
<button onclick="ttydPaneNav('Down')" title="Panneau bas"></button>
<div id="ttyd-spacer"></div>
<span style="color:#333;font:10px monospace;">Ctrl+C=copier/inter. &nbsp; Ctrl+V=coller &nbsp; clic droit=menu</span>
</div>
<div id="ttyd-search-bar">
<input id="ttyd-sq" placeholder="Rechercher…" oninput="ttydDoSearch()" onkeydown="ttydSearchKey(event)">
<span id="ttyd-sr"></span>
<button onclick="ttydSearchNav(-1)"></button>
<button onclick="ttydSearchNav(+1)"></button>
<button onclick="ttydSearchClose()"></button>
</div>
<div id="ttyd-ctx"></div>
<div id="ttyd-toast"></div>
<script>
(function(){
var _fontSize = parseInt(localStorage.getItem('ttyd-font') || '13');
var _themes = {"Defaut": {"background": "#2b2b2b", "foreground": "#d2d2d2", "cursor": "#adadad", "black": "#000000", "red": "#d81e00", "green": "#5ea702", "yellow": "#cfae00", "blue": "#427ab3", "magenta": "#89658e", "cyan": "#00a7aa", "white": "#dbded8", "brightBlack": "#686a66", "brightRed": "#f54235", "brightGreen": "#99e343", "brightYellow": "#fdeb61", "brightBlue": "#84b0d8", "brightMagenta": "#bc94b7", "brightCyan": "#37e6e8", "brightWhite": "#f1f1f0"}, "Dracula": {"background": "#282a36", "foreground": "#f8f8f2", "cursor": "#f8f8f2", "black": "#21222c", "red": "#ff5555", "green": "#50fa7b", "yellow": "#f1fa8c", "blue": "#bd93f9", "magenta": "#ff79c6", "cyan": "#8be9fd", "white": "#f8f8f2", "brightBlack": "#6272a4", "brightRed": "#ff6e6e", "brightGreen": "#69ff94", "brightYellow": "#ffffa5", "brightBlue": "#d6acff", "brightMagenta": "#ff92df", "brightCyan": "#a4ffff", "brightWhite": "#ffffff"}, "One Dark": {"background": "#282c34", "foreground": "#abb2bf", "cursor": "#528bff", "black": "#3f4451", "red": "#e06c75", "green": "#98c379", "yellow": "#e5c07b", "blue": "#61afef", "magenta": "#c678dd", "cyan": "#56b6c2", "white": "#abb2bf", "brightBlack": "#4f5666", "brightRed": "#ff616e", "brightGreen": "#a5e075", "brightYellow": "#f0a45d", "brightBlue": "#4dc4ff", "brightMagenta": "#de73ff", "brightCyan": "#4cd1e0", "brightWhite": "#ffffff"}, "Solarized": {"background": "#002b36", "foreground": "#839496", "cursor": "#839496", "black": "#073642", "red": "#dc322f", "green": "#859900", "yellow": "#b58900", "blue": "#268bd2", "magenta": "#d33682", "cyan": "#2aa198", "white": "#eee8d5", "brightBlack": "#002b36", "brightRed": "#cb4b16", "brightGreen": "#586e75", "brightYellow": "#657b83", "brightBlue": "#839496", "brightMagenta": "#6c71c4", "brightCyan": "#93a1a1", "brightWhite": "#fdf6e3"}, "Clair": {"background": "#ffffff", "foreground": "#383a42", "cursor": "#526fff", "black": "#383a42", "red": "#e45649", "green": "#50a14f", "yellow": "#c18401", "blue": "#4078f2", "magenta": "#a626a4", "cyan": "#0184bc", "white": "#fafafa", "brightBlack": "#4f525e", "brightRed": "#e06c75", "brightGreen": "#98c379", "brightYellow": "#e5c07b", "brightBlue": "#61afef", "brightMagenta": "#c678dd", "brightCyan": "#56b6c2", "brightWhite": "#ffffff"}};
var _themeName = localStorage.getItem('ttyd-theme') || 'Defaut';
var _searchMatches = [], _searchIdx = 0;
var _notifPerm = false;
var _lastCmdStart = 0;
/* ---- Toast ---- */
function toast(msg, dur) {
var t = document.getElementById('ttyd-toast');
t.textContent = msg; t.classList.add('show');
clearTimeout(t._tid);
t._tid = setTimeout(function(){ t.classList.remove('show'); }, dur||1800);
}
/* ---- Clipboard ---- */
function writeClip(text) {
if (!text) return;
var ok = function(){ toast('Copié : ' + text.slice(0,40).replace(/\n/g,' ') + (text.length>40?'..':'')); };
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(text).then(ok).catch(function(){ legacyCopy(text); });
else legacyCopy(text);
}
function legacyCopy(text) {
var ta = document.createElement('textarea');
ta.value = text; ta.style.cssText='position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;';
document.body.appendChild(ta); ta.focus(); ta.select();
var ok=false; try{ ok=document.execCommand('copy'); }catch(e){}
document.body.removeChild(ta);
toast(ok ? 'Copié (legacy)' : 'Erreur copie');
}
function readClip(cb) {
if (navigator.clipboard && navigator.clipboard.readText)
navigator.clipboard.readText().then(cb).catch(function(){ toast('Accès presse-papier refusé'); });
else toast('API clipboard non disponible');
}
/* ---- Police ---- */
window.ttydFontSize = function(d) {
_fontSize = Math.max(8, Math.min(28, _fontSize + d));
localStorage.setItem('ttyd-font', _fontSize);
if (window.term) { window.term.options.fontSize = _fontSize; window.dispatchEvent(new Event('resize')); }
document.getElementById('ttyd-fsize').textContent = _fontSize;
};
/* ---- Clear ---- */
window.ttydClear = function() { if (window.term) window.term.clear(); };
/* ---- Tmux split & navigation ---- */
function sendRaw(data) {
// Envoie données brutes au PTY sans bracketed paste
var term = window.term;
if (!term) return;
if (term._core && term._core.coreService)
term._core.coreService.triggerDataEvent(data, true);
else
term.paste(data);
}
window.ttydSplit = function(dir) {
// Ctrl+B (\x02) + % pour vertical, + " pour horizontal
sendRaw('\x02' + (dir === 'h' ? '%' : '"'));
};
window.ttydPaneClose = function() {
// Ctrl+B + x, puis y pour confirmer
sendRaw('\x02x');
setTimeout(function(){ sendRaw('y'); }, 200);
};
window.ttydPaneNav = function(dir) {
var arrows = {Left:'\x1b[D', Right:'\x1b[C', Up:'\x1b[A', Down:'\x1b[B'};
if (arrows[dir]) sendRaw('\x02' + arrows[dir]);
};
/* ---- Thèmes ---- */
var sel = document.getElementById('ttyd-theme-sel');
Object.keys(_themes).forEach(function(n){
var o = document.createElement('option'); o.value=n; o.textContent=n;
if(n===_themeName) o.selected=true;
sel.appendChild(o);
});
window.ttydSetTheme = function(name) {
if (!_themes[name] || !window.term) return;
window.term.options.theme = _themes[name];
_themeName = name;
localStorage.setItem('ttyd-theme', name);
toast('Thème : ' + name);
};
/* ---- Recherche ---- */
var _searchOpen = false;
window.ttydSearch = function() {
var bar = document.getElementById('ttyd-search-bar');
_searchOpen = !_searchOpen;
bar.style.display = _searchOpen ? 'flex' : 'none';
if (_searchOpen) setTimeout(function(){ document.getElementById('ttyd-sq').focus(); }, 50);
};
window.ttydSearchClose = function() {
_searchOpen = false;
document.getElementById('ttyd-search-bar').style.display = 'none';
_searchMatches = []; document.getElementById('ttyd-sr').textContent = '—';
};
window.ttydDoSearch = function() {
var q = document.getElementById('ttyd-sq').value;
var sr = document.getElementById('ttyd-sr');
if (!q || !window.term) { _searchMatches=[]; sr.textContent='—'; return; }
var buf = window.term.buffer.active;
var matches = [];
var total = buf.length;
for (var i = 0; i < total; i++) {
var line = buf.getLine(i);
if (line && line.translateToString().indexOf(q) >= 0) matches.push(i);
}
_searchMatches = matches; _searchIdx = matches.length - 1;
sr.textContent = matches.length ? ((_searchIdx+1)+'/'+matches.length) : '0 résultat';
if (matches.length) window.term.scrollToLine(matches[_searchIdx]);
};
window.ttydSearchNav = function(dir) {
if (!_searchMatches.length) return;
_searchIdx = (_searchIdx + dir + _searchMatches.length) % _searchMatches.length;
document.getElementById('ttyd-sr').textContent = (_searchIdx+1)+'/'+_searchMatches.length;
window.term.scrollToLine(_searchMatches[_searchIdx]);
};
window.ttydSearchKey = function(e) {
if (e.key === 'Enter') ttydSearchNav(e.shiftKey ? -1 : 1);
if (e.key === 'Escape') ttydSearchClose();
};
/* ---- Connexion ---- */
var _connEl = document.getElementById('ttyd-conn');
function checkConn() {
fetch('/token', {cache:'no-store'})
.then(function(r){ _connEl.className = r.ok ? 'ok' : 'err'; })
.catch(function(){ _connEl.className = 'err'; });
}
checkConn();
setInterval(checkConn, 5000);
/* ---- Patch window.open (liens WebLinksAddon) ---- */
var _open = window.open;
window.open = function(url, target, features) {
if (url === undefined || url === null || url === '') {
var loc = {};
Object.defineProperty(loc,'href',{set:function(u){
var a=document.createElement('a'); a.href=u; a.target='_blank'; a.rel='noopener noreferrer';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
}});
return {opener:null, location:loc};
}
return _open.apply(this, arguments);
};
/* ---- Menu contextuel ---- */
var _ctx = document.getElementById('ttyd-ctx');
function hideCtx(){ _ctx.style.display='none'; }
document.addEventListener('click', hideCtx);
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ hideCtx(); ttydSearchClose(); }});
function showCtx(x,y) {
hideCtx();
_ctx.innerHTML = '';
var items = [
['Copier', function(){ if(window.term){var s=window.term.getSelection();if(s)writeClip(s);} }],
['Coller', function(){ if(window.term) readClip(function(t){window.term.paste(t);}); }],
null,
['Rechercher…', ttydSearch],
null,
['Effacer', ttydClear],
['Police +', function(){ttydFontSize(+1);}],
['Police ', function(){ttydFontSize(-1);}],
];
items.forEach(function(item){
if(!item){var hr=document.createElement('hr');_ctx.appendChild(hr);return;}
var d=document.createElement('div'); d.textContent=item[0];
d.addEventListener('click',function(e){e.stopPropagation();hideCtx();item[1]();});
_ctx.appendChild(d);
});
_ctx.style.cssText='display:block;left:'+x+'px;top:'+y+'px;';
var r=_ctx.getBoundingClientRect();
if(r.right>window.innerWidth) _ctx.style.left=(x-r.width)+'px';
if(r.bottom>window.innerHeight) _ctx.style.top=(y-r.height)+'px';
}
/* ---- Notifications ---- */
function requestNotifPerm() {
if ('Notification' in window && Notification.permission === 'default')
Notification.requestPermission().then(function(p){ _notifPerm = p==='granted'; });
else _notifPerm = ('Notification' in window && Notification.permission === 'granted');
}
function desktopNotif(title, body) {
if (_notifPerm && 'Notification' in window)
new Notification(title, {body:body, icon:''});
}
requestNotifPerm();
/* ---- Initialisation terminal ---- */
function waitForTerm() {
var term = window.term;
if (!term || !term.parser) { setTimeout(waitForTerm, 100); return; }
/* Police initiale */
term.options.fontSize = _fontSize;
document.getElementById('ttyd-fsize').textContent = _fontSize;
/* Thème initial */
if (_themes[_themeName]) term.options.theme = _themes[_themeName];
/* Refit : le fit initial a ete calcule avec l'ancienne taille de cellule */
setTimeout(function(){ window.dispatchEvent(new Event('resize')); }, 50);
/* OSC 52 : tmux set-clipboard -> presse-papier */
try {
term.parser.registerOscHandler(52, function(data) {
var parts = data.split(';');
if (parts.length>=2 && parts[1] && parts[1]!=='?') {
try { writeClip(atob(parts[1])); } catch(e){}
}
return true;
});
} catch(e){}
/* OSC 777 : PROMPT_COMMAND -> notification fin de commande */
try {
term.parser.registerOscHandler(777, function(data) {
var code = parseInt(data) || 0;
var elapsed = Date.now() - _lastCmdStart;
/* Notifier si onglet caché ET commande > 5s */
if (document.hidden && elapsed > 5000) {
var msg = code === 0 ? 'Commande terminée (succès)' : 'Commande terminée (code '+code+')';
desktopNotif('Terminal', msg);
}
_lastCmdStart = Date.now();
return true;
});
} catch(e){}
/* Ctrl+C = copie si sélection, sinon SIGINT */
/* Ctrl+V = colle */
/* Ctrl+F = recherche */
term.attachCustomKeyEventHandler(function(e) {
if (e.type !== 'keydown') return true;
if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
if (e.code==='KeyC') {
var s = term.getSelection ? term.getSelection() : '';
if (s) { writeClip(s); return false; }
return true;
}
if (e.code==='KeyV') {
e.preventDefault();
readClip(function(t){ term.paste(t); }); return false;
}
if (e.code==='KeyF') { ttydSearch(); return false; }
}
if (e.ctrlKey && e.shiftKey) {
if (e.code==='KeyC') { var s=term.getSelection?term.getSelection():''; if(s) writeClip(s); return false; }
if (e.code==='KeyV') { e.preventDefault(); readClip(function(t){ term.paste(t); }); return false; }
}
return true;
});
/* Clic droit -> menu contextuel */
term.element.addEventListener('contextmenu', function(e){
e.preventDefault(); showCtx(e.clientX, e.clientY);
});
/* Zoom souris */
term.element.addEventListener('wheel', function(e){
if (!e.ctrlKey) return;
e.preventDefault();
ttydFontSize(e.deltaY < 0 ? 1 : -1);
}, {passive:false});
}
waitForTerm();
})();
</script>