move python clide to legacy/

Clide is being rebuilt as a Flutter desktop app. The Python Textual
implementation moves wholesale into legacy/ rather than being deleted:
its pane model, panel set, git skills, and panel communication design
are real thought that should remain readable next to the new code
while the rebuild finds its shape. Git's rename tracking preserves
history, so `git log -- legacy/` still works.

The Flutter rebuild lives at the repo root alongside a Go sidecar
(the architecture claudian was heading toward, which folds into
clide as a core component rather than a separate plugin project).
Bootstrap of the new shape lands in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 20:30:51 +02:00
co-authored by Claude Opus 4.7
parent 30b8cf7db9
commit a355751437
163 changed files with 0 additions and 0 deletions
@@ -0,0 +1,388 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Clide</title>
<link rel="stylesheet" href="/static/vendor/xterm.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #21262f;
font-family: 'JetBrains Mono', monospace;
/* Kill all scrollbars */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
/* Kill xterm.js internal scrollbar — TUI handles its own scrolling */
.xterm-viewport {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
.xterm-viewport::-webkit-scrollbar {
display: none !important;
}
/* Toolbar */
#toolbar {
height: 36px;
background: #292e38;
border-bottom: 1px solid #393e48;
display: flex;
align-items: center;
padding: 0 12px;
gap: 12px;
font-family: system-ui, -apple-system, sans-serif;
color: #e2e8f5;
font-size: 13px;
user-select: none;
}
#toolbar .logo {
font-weight: 600;
color: #00a3d2;
font-family: 'JetBrains Mono', monospace;
letter-spacing: 0.5px;
}
#toolbar .separator {
width: 1px;
height: 20px;
background: #393e48;
}
#toolbar select {
background: #393e48;
color: #e2e8f5;
border: 1px solid #525762;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
font-family: system-ui, -apple-system, sans-serif;
cursor: pointer;
outline: none;
}
#toolbar select:hover {
border-color: #00a3d2;
}
#toolbar select:focus {
border-color: #00a3d2;
box-shadow: 0 0 0 1px #00a3d2;
}
#toolbar label {
font-size: 11px;
color: #898e9a;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 6px;
}
.toolbar-spacer { flex: 1; }
#status {
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
}
#status .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #525762;
transition: background 0.3s;
}
#status .dot.connected { background: #00ab9a; }
#status .dot.reconnecting { background: #d08447; animation: pulse 1s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Terminal container */
#terminal-container {
height: calc(100% - 36px);
width: 100%;
position: absolute;
top: 36px;
left: 0;
right: 0;
bottom: 0;
}
/* Reconnect overlay */
#overlay {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(33, 38, 47, 0.92);
z-index: 100;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 12px;
color: #e2e8f5;
font-family: system-ui, -apple-system, sans-serif;
}
#overlay .message {
font-size: 16px;
font-weight: 500;
}
#overlay .sub {
font-size: 13px;
color: #898e9a;
}
</style>
</head>
<body>
<div id="toolbar">
<span class="logo">Clide</span>
<div class="separator"></div>
<div class="toolbar-group">
<label>Project</label>
<select id="project-select"><option value="">Loading...</option></select>
</div>
<div class="toolbar-spacer"></div>
<div id="status">
<div class="dot" id="status-dot"></div>
<span id="status-text">Connecting...</span>
</div>
</div>
<div id="terminal-container"></div>
<div id="overlay">
<div class="message">Reconnecting...</div>
<div class="sub">Session is preserved</div>
</div>
<script src="/static/vendor/xterm.min.js"></script>
<script src="/static/vendor/addon-fit.min.js"></script>
<script src="/static/vendor/addon-web-links.min.js"></script>
<script>
(function() {
"use strict";
// --- State ---
// Extract project from path: /projects/<name> or fallback to ?project=<name>
function getProjectFromUrl() {
var match = location.pathname.match(/^\/projects\/([^/]+)/);
if (match) return decodeURIComponent(match[1]);
return new URLSearchParams(location.search).get("project") || "";
}
let currentProject = getProjectFromUrl();
let ws = null;
let reconnectTimer = null;
let reconnectDelay = 1000;
// --- DOM refs ---
const statusDot = document.getElementById("status-dot");
const statusText = document.getElementById("status-text");
const overlay = document.getElementById("overlay");
const projectSelect = document.getElementById("project-select");
// --- Terminal ---
const term = new Terminal({
fontFamily: "'JetBrains Mono', monospace",
fontSize: 14,
theme: {
background: "#21262f",
foreground: "#e2e8f5",
cursor: "#00a3d2",
cursorAccent: "#21262f",
selectionBackground: "rgba(0, 163, 210, 0.3)",
black: "#21262f",
red: "#f06c6f",
green: "#00ab9a",
yellow: "#d08447",
blue: "#00a3d2",
magenta: "#fa5f8b",
cyan: "#00a9b9",
white: "#e2e8f5",
brightBlack: "#393e48",
brightRed: "#f06c6f",
brightGreen: "#00ab9a",
brightYellow: "#d08447",
brightBlue: "#00a3d2",
brightMagenta: "#fa5f8b",
brightCyan: "#00a9b9",
brightWhite: "#e2e8f5",
},
cursorBlink: true,
allowProposedApi: true,
scrollback: 0,
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon.WebLinksAddon());
term.open(document.getElementById("terminal-container"));
fitAddon.fit();
// --- Status helpers ---
function setStatus(state, text) {
statusDot.className = "dot " + state;
statusText.textContent = text;
}
// --- WebSocket ---
function connect() {
if (ws) {
ws.onclose = null;
ws.close();
}
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const url = proto + "//" + location.host + "/projects/" + encodeURIComponent(currentProject) + "/ws";
ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
ws.onopen = function() {
setStatus("connected", currentProject || "Connected");
overlay.style.display = "none";
reconnectDelay = 1000;
// Send initial terminal size (slight delay to ensure xterm is rendered)
setTimeout(function() {
fitAddon.fit();
sendResize();
}, 50);
};
ws.onmessage = function(evt) {
let prefix, payload;
if (evt.data instanceof ArrayBuffer) {
const bytes = new Uint8Array(evt.data);
if (bytes.length < 1) return;
prefix = String.fromCharCode(bytes[0]);
payload = bytes.slice(1);
if (prefix === "0") {
term.write(payload);
} else if (prefix === "1") {
handleControl(new TextDecoder().decode(payload));
}
} else {
// Text frame
prefix = evt.data[0];
payload = evt.data.slice(1);
if (prefix === "0") {
term.write(payload);
} else if (prefix === "1") {
handleControl(payload);
}
}
};
ws.onclose = function() {
setStatus("reconnecting", "Reconnecting...");
overlay.style.display = "flex";
reconnectTimer = setTimeout(function() {
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
connect();
}, reconnectDelay);
};
ws.onerror = function() {
// onclose will fire after this
};
}
// --- Terminal input → WebSocket ---
term.onData(function(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send("0" + data);
}
});
// --- Resize ---
function sendResize() {
if (ws && ws.readyState === WebSocket.OPEN) {
const dims = fitAddon.proposeDimensions();
if (dims) {
ws.send("2" + dims.cols + "," + dims.rows);
}
}
}
const resizeObserver = new ResizeObserver(function() {
fitAddon.fit();
sendResize();
});
resizeObserver.observe(document.getElementById("terminal-container"));
// --- Control messages ---
function handleControl(jsonStr) {
let msg;
try { msg = JSON.parse(jsonStr); } catch(e) { return; }
if (msg.type === "projects") {
populateProjects(msg.projects);
} else if (msg.type === "session_info") {
setStatus("connected", msg.project);
document.title = "Clide — " + msg.project;
} else if (msg.type === "error") {
term.write("\r\n\x1b[31mError: " + msg.message + "\x1b[0m\r\n");
}
}
// --- Project management ---
function populateProjects(projects) {
projectSelect.innerHTML = "";
projects.forEach(function(p) {
const opt = document.createElement("option");
opt.value = p;
opt.textContent = p;
if (p === currentProject) opt.selected = true;
projectSelect.appendChild(opt);
});
}
projectSelect.addEventListener("change", function() {
currentProject = projectSelect.value;
history.pushState(null, "", "/projects/" + encodeURIComponent(currentProject));
document.title = "Clide — " + currentProject;
if (reconnectTimer) clearTimeout(reconnectTimer);
term.clear();
term.reset();
connect();
});
// --- Load projects and connect ---
fetch("/api/projects")
.then(function(r) { return r.json(); })
.then(function(data) {
populateProjects(data.projects);
// If no project selected, pick first available
if (!currentProject && data.projects.length > 0) {
currentProject = data.projects[0];
projectSelect.value = currentProject;
history.replaceState(null, "", "/projects/" + encodeURIComponent(currentProject));
}
if (currentProject) {
connect();
} else {
setStatus("", "No projects found");
term.write("\r\nNo projects found in configured projects directory.\r\n");
}
})
.catch(function() {
setStatus("reconnecting", "Server unreachable");
});
})();
</script>
</body>
</html>
@@ -0,0 +1,8 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/@xterm/addon-fit@0.11.0/lib/addon-fit.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(globalThis,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core._renderService.dimensions;if(0===e.css.cell.width||0===e.css.cell.height)return;const t=0===this._terminal.options.scrollback?0:this._terminal.options.overviewRuler?.width||14,r=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(r.getPropertyValue("height")),o=Math.max(0,parseInt(r.getPropertyValue("width"))),s=window.getComputedStyle(this._terminal.element),n=i-(parseInt(s.getPropertyValue("padding-top"))+parseInt(s.getPropertyValue("padding-bottom"))),l=o-(parseInt(s.getPropertyValue("padding-right"))+parseInt(s.getPropertyValue("padding-left")))-t;return{cols:Math.max(2,Math.floor(l/e.css.cell.width)),rows:Math.max(1,Math.floor(n/e.css.cell.height))}}}})(),e})()));
//# sourceMappingURL=addon-fit.js.map
@@ -0,0 +1,8 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/@xterm/addon-web-links@0.12.0/lib/addon-web-links.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(globalThis,(()=>(()=>{"use strict";var e={490:(e,t)=>{function n(e){try{const t=new URL(e),n=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(n.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,o={}){this._terminal=e,this._regex=t,this._handler=n,this._options=o}provideLinks(e,t){const n=o.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:o}=e;this._options.hover(t,n,o)}},e)))}};class o{static computeLink(e,t,r,i){const s=new RegExp(t.source,(t.flags||"")+"g"),[a,c]=o._getWindowedLineStrings(e-1,r),l=a.join("");let d;const p=[];for(;d=s.exec(l);){const e=d[0];if(!n(e))continue;const[t,s]=o._mapStrIdx(r,c,0,d.index),[a,l]=o._mapStrIdx(r,t,s,e.length);if(-1===t||-1===s||-1===a||-1===l)continue;const h={start:{x:s+1,y:t+1},end:{x:l,y:a+1}};p.push({range:h,text:e,activate:i})}return p}static _getWindowedLineStrings(e,t){let n,o=e,r=e,i=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),i=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,o]}static _mapStrIdx(e,t,n,o){const r=e.buffer.active,i=r.getNullCell();let s=n;for(;o;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,i);const s=i.getChars();if(i.getWidth()&&(o-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,i),2===i.getWidth()&&(o+=1))}if(o<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=o}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(490),r=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function i(e,t){const n=window.open();if(n){try{n.opener=null}catch{}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=i,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,o=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,o,this._handler,n))}dispose(){this._linkProvider?.dispose()}}})(),o})()));
//# sourceMappingURL=addon-web-links.js.map
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using clean-css v5.3.3.
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
/*# sourceMappingURL=/sm/97377c0c258e109358121823f5790146c714989366481f90e554c42277efb500.map */
File diff suppressed because one or more lines are too long