Phase 8: TypeScript migration, i18n rewrite, Activity Tree, Projects API, Heartbeats

This commit is contained in:
Rose
2026-04-29 11:50:00 +02:00
parent c705fad626
commit 255914c9f1
43 changed files with 17948 additions and 6899 deletions

View File

@@ -1,438 +1,514 @@
async function api(path,opts={}){
// Strip leading slash so URL resolves relative to location.href (supports subpath mounts)
const rel = path.startsWith('/') ? path.slice(1) : path;
const url=new URL(rel,location.href);
const res=await fetch(url.href,{credentials:'include',headers:{'Content-Type':'application/json'},...opts});
if(!res.ok){
const text=await res.text();
// Parse JSON error body and surface the human-readable message,
// rather than showing raw JSON like {"error":"Profile 'x' does not exist."}
try{const j=JSON.parse(text);throw new Error(j.error||j.message||text);}
catch(e){if(e instanceof SyntaxError)throw new Error(text);throw e;}
(() => {
const IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", ".bmp"]);
const MD_EXTS = /* @__PURE__ */ new Set([".md", ".markdown", ".mdown"]);
const DOWNLOAD_EXTS = /* @__PURE__ */ new Set([
".docx",
".doc",
".xlsx",
".xls",
".pptx",
".ppt",
".odt",
".ods",
".odp",
".pdf",
".zip",
".tar",
".gz",
".bz2",
".7z",
".rar",
".mp3",
".mp4",
".wav",
".m4a",
".ogg",
".flac",
".mov",
".avi",
".mkv",
".webm",
".exe",
".dmg",
".pkg",
".deb",
".rpm",
".woff",
".woff2",
".ttf",
".otf",
".eot",
".bin",
".dat",
".db",
".sqlite",
".pyc",
".class",
".so",
".dylib",
".dll"
]);
window.IMAGE_EXTS = IMAGE_EXTS;
window.MD_EXTS = MD_EXTS;
window.DOWNLOAD_EXTS = DOWNLOAD_EXTS;
function fileExt(p) {
const i = p.lastIndexOf(".");
return i >= 0 ? p.slice(i).toLowerCase() : "";
}
const ct=res.headers.get('content-type')||'';
return ct.includes('application/json')?res.json():res.text();
}
// Persist/restore expanded directory state per workspace in localStorage
function _wsExpandKey(){
const ws=S.session&&S.session.workspace;
return ws?'hermes-webui-expanded:'+ws:null;
}
function _saveExpandedDirs(){
const key=_wsExpandKey();if(!key)return;
try{localStorage.setItem(key,JSON.stringify([...(S._expandedDirs||new Set())]));}catch(e){}
}
function _restoreExpandedDirs(){
const key=_wsExpandKey();
if(!key){S._expandedDirs=new Set();return;}
try{
const raw=localStorage.getItem(key);
S._expandedDirs=raw?new Set(JSON.parse(raw)):new Set();
}catch(e){S._expandedDirs=new Set();}
}
async function loadDir(path){
if(!S.session)return;
try{
if(!path||path==='.'){
S._dirCache={};
_restoreExpandedDirs(); // restore per-workspace expanded state on root load
}
S.currentDir=path||'.';
const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
S.entries=data.entries||[];renderBreadcrumb();renderFileTree();
// Pre-fetch contents of restored expanded dirs so they render without a second click
if(!path||path==='.'){
for(const dirPath of (S._expandedDirs||[])){
if(!S._dirCache[dirPath]){
try{
const dc=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(dirPath)}`);
S._dirCache[dirPath]=dc.entries||[];
}catch(e2){S._dirCache[dirPath]=[];}
}
}
if(S._expandedDirs&&S._expandedDirs.size>0)renderFileTree();
}
if(typeof clearPreview==='function'){
if(typeof _previewDirty!=='undefined'&&_previewDirty){
showConfirmDialog({title:t('unsaved_confirm'),message:'',confirmLabel:'Discard',danger:true,focusCancel:true}).then(ok=>{if(ok)clearPreview();});
}else{
clearPreview();
window.fileExt = fileExt;
async function api(path, opts = {}) {
const rel = path.startsWith("/") ? path.slice(1) : path;
const url = new URL(rel, location.href);
const res = await fetch(url.href, { credentials: "include", headers: { "Content-Type": "application/json" }, ...opts });
if (!res.ok) {
const text = await res.text();
try {
const j = JSON.parse(text);
throw new Error(j.error || j.message || text);
} catch (e) {
if (e instanceof SyntaxError) throw new Error(text);
throw e;
}
}
// Fetch git info for workspace root (non-blocking)
if(!path||path==='.') _refreshGitBadge();
}catch(e){console.warn('loadDir',e);}
}
async function _refreshGitBadge(){
const badge=$('gitBadge');
if(!badge||!S.session)return;
try{
const data=await api(`/api/git-info?session_id=${encodeURIComponent(S.session.session_id)}`);
if(data.git&&data.git.is_git){
const g=data.git;
let text=g.branch||'git';
if(g.dirty>0) text+=` \u00b7 ${g.dirty}\u2206`; // middot + delta
if(g.behind>0) text+=` \u2193${g.behind}`;
if(g.ahead>0) text+=` \u2191${g.ahead}`;
badge.textContent=text;
badge.className='git-badge'+(g.dirty>0?' dirty':'');
badge.style.display='';
} else {
badge.style.display='none';
badge.textContent='';
}
}catch(e){badge.style.display='none';}
}
function navigateUp(){
if(!S.session||S.currentDir==='.')return;
const parts=S.currentDir.split('/');
parts.pop();
loadDir(parts.length?parts.join('/'):'.');
}
// File extension sets for preview routing (must match server-side sets)
const IMAGE_EXTS = new Set(['.png','.jpg','.jpeg','.gif','.svg','.webp','.ico','.bmp']);
const MD_EXTS = new Set(['.md','.markdown','.mdown']);
// Binary formats that should download rather than preview
const DOWNLOAD_EXTS = new Set([
'.docx','.doc','.xlsx','.xls','.pptx','.ppt','.odt','.ods','.odp',
'.pdf','.zip','.tar','.gz','.bz2','.7z','.rar',
'.mp3','.mp4','.wav','.m4a','.ogg','.flac','.mov','.avi','.mkv','.webm',
'.exe','.dmg','.pkg','.deb','.rpm',
'.woff','.woff2','.ttf','.otf','.eot',
'.bin','.dat','.db','.sqlite','.pyc','.class','.so','.dylib','.dll',
]);
function fileExt(p){ const i=p.lastIndexOf('.'); return i>=0?p.slice(i).toLowerCase():''; }
let _previewCurrentPath = ''; // relative path of currently previewed file
let _previewCurrentMode = ''; // 'code' | 'md' | 'image'
let _previewDirty = false; // true when edits are unsaved
function showPreview(mode){
// mode: 'code' | 'image' | 'md'
$('previewCode').style.display = mode==='code' ? '' : 'none';
$('previewImgWrap').style.display = mode==='image' ? '' : 'none';
$('previewMd').style.display = mode==='md' ? '' : 'none';
$('previewEditArea').style.display = 'none'; // start in read-only
const badge=$('previewBadge');
badge.className='preview-badge '+mode;
badge.textContent = mode==='image'?'image':mode==='md'?'md':fileExt($('previewPathText').textContent)||'text';
_previewCurrentMode = mode;
_previewDirty = false;
updateEditBtn();
}
function updateEditBtn(){
const btn=$('btnEditFile');
if(!btn)return;
const editable = _previewCurrentMode==='code'||_previewCurrentMode==='md';
btn.style.display = editable?'':'none';
const editing = $('previewEditArea').style.display!=='none';
btn.innerHTML = editing ? `💾 ${t('save')}` : `✎ ${t('edit')}`;
btn.title = editing ? t('save_title') : t('edit_title');
btn.style.color = editing ? 'var(--blue)' : '';
if(_previewDirty) btn.innerHTML = '💾 Save*';
}
async function toggleEditMode(){
const editing = $('previewEditArea').style.display!=='none';
if(editing){
// Save
if(!S.session||!_previewCurrentPath)return;
const content=$('previewEditArea').value;
try{
await api('/api/file/save',{method:'POST',body:JSON.stringify({
session_id:S.session.session_id, path:_previewCurrentPath, content
})});
_previewDirty=false;
// Update read-only views
if(_previewCurrentMode==='code') $('previewCode').textContent=content;
else { $('previewMd').innerHTML=renderMd(content); requestAnimationFrame(()=>{if(typeof renderKatexBlocks==='function')renderKatexBlocks();}); }
$('previewEditArea').style.display='none';
if(_previewCurrentMode==='code') $('previewCode').style.display='';
else $('previewMd').style.display='';
showToast(t('saved'));
}catch(e){setStatus(t('save_failed')+e.message);}
}else{
// Enter edit mode: populate textarea with current content
const currentText = _previewCurrentMode==='code'
? $('previewCode').textContent
: _previewRawContent||'';
$('previewEditArea').value=currentText;
$('previewEditArea').style.display='';
if(_previewCurrentMode==='code') $('previewCode').style.display='none';
else $('previewMd').style.display='none';
// Escape cancels the edit without saving
$('previewEditArea').onkeydown=e=>{
if(e.key==='Escape'){e.preventDefault();cancelEditMode();}
};
const ct = res.headers.get("content-type") || "";
return ct.includes("application/json") ? res.json() : res.text();
}
updateEditBtn();
}
let _previewRawContent = ''; // raw text for md files (to populate editor)
function cancelEditMode(){
// Discard changes and return to read-only view
$('previewEditArea').style.display='none';
$('previewEditArea').onkeydown=null;
if(_previewCurrentMode==='code') $('previewCode').style.display='';
else $('previewMd').style.display='';
_previewDirty=false;
updateEditBtn();
}
async function openFile(path){
if(!S.session)return;
const ext=fileExt(path);
// Binary/download-only formats: trigger browser download, don't preview
if(DOWNLOAD_EXTS.has(ext)){
downloadFile(path);
return;
window.api = api;
function _wsExpandKey() {
const ws = S.session && S.session.workspace;
return ws ? "hermes-webui-expanded:" + ws : null;
}
$('previewPathText').textContent=path;
$('previewArea').classList.add('visible');
$('fileTree').style.display='none';
const wsSearch=$('wsSearchWrap');if(wsSearch)wsSearch.style.display='none';
_previewCurrentPath = path;
renderFileBreadcrumb(path);
if(IMAGE_EXTS.has(ext)){
// Image: load via raw endpoint, show as <img>
showPreview('image');
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`;
$('previewImg').alt=path;
$('previewImg').src=url;
$('previewImg').onerror=()=>setStatus(t('image_load_failed'));
} else if(MD_EXTS.has(ext)){
// Markdown: fetch text, render with renderMd, display as formatted HTML
try{
const data=await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
showPreview('md');
_previewRawContent = data.content;
$('previewMd').innerHTML=renderMd(data.content);
requestAnimationFrame(()=>{if(typeof renderKatexBlocks==='function')renderKatexBlocks();});
}catch(e){setStatus(t('file_open_failed'));}
} else {
// Plain code / text -- but fall back to download if server signals binary
try{
const data=await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
if(data.binary){
// Server flagged this as binary content
downloadFile(path);
return;
}
showPreview('code');
// Apply syntax highlighting based on file extension
const content = data.content || '';
if(['yml','yaml'].includes(ext)){
$('previewCode').className='preview-code hl-yaml';
if(typeof highlightYAML==='function'){
$('previewCode').innerHTML=highlightYAML(content);
}else{
$('previewCode').innerHTML=_highlightWithLineNumbers(content);
}
}else if(ext==='json'){
$('previewCode').className='preview-code hl-json';
$('previewCode').innerHTML=_highlightJSON(content);
}else if(['py','js','ts','sh','bash','zsh','rb','go','rs','java','c','cpp','h','css','scss','html','xml','sql','r','lua','pl','php','swift','kt','dart'].includes(ext)){
$('previewCode').className='preview-code';
$('previewCode').textContent=content;
requestAnimationFrame(()=>{if(typeof highlightCode==='function')highlightCode();});
}else{
// txt, toml, cfg, ini, conf, env, log, etc — readable with line numbers
$('previewCode').className='preview-code hl-text';
$('previewCode').innerHTML=_highlightWithLineNumbers(content);
}
}catch(e){
// If it's a 400/too-large error, offer download instead
downloadFile(path);
function _saveExpandedDirs() {
const key = _wsExpandKey();
if (!key) return;
try {
localStorage.setItem(key, JSON.stringify([...S._expandedDirs || /* @__PURE__ */ new Set()]));
} catch (e) {
}
}
}
function downloadFile(path){
if(!S.session)return;
// Trigger browser download via the raw file endpoint with content-disposition attachment
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&download=1`;
const filename=path.split('/').pop();
const a=document.createElement('a');
a.href=url;a.download=filename;
document.body.appendChild(a);a.click();
setTimeout(()=>document.body.removeChild(a),100);
showToast(t('downloading',filename),2000);
}
// ── Render breadcrumb for file preview mode ──────────────────────────────────
function renderFileBreadcrumb(filePath) {
const bar = $('breadcrumbBar');
if (!bar) return;
bar.style.display = 'flex';
const upBtn = $('btnUpDir');
if (upBtn) upBtn.style.display = '';
bar.innerHTML = '';
// Root
const root = document.createElement('span');
root.className = 'breadcrumb-seg breadcrumb-link';
root.textContent = '~';
root.onclick = () => { clearPreview(); loadDir('.'); };
bar.appendChild(root);
const parts = filePath.split('/');
let accumulated = '';
for (let i = 0; i < parts.length; i++) {
const sep = document.createElement('span');
sep.className = 'breadcrumb-sep';
sep.textContent = '/';
bar.appendChild(sep);
accumulated += (accumulated ? '/' : '') + parts[i];
const seg = document.createElement('span');
seg.textContent = parts[i];
if (i < parts.length - 1) {
seg.className = 'breadcrumb-seg breadcrumb-link';
const target = accumulated;
seg.onclick = () => { clearPreview(); loadDir(target); };
} else {
seg.className = 'breadcrumb-seg breadcrumb-current';
}
bar.appendChild(seg);
}
}
// ── Syntax highlighting helpers for file preview ──
function _escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function _highlightWithLineNumbers(text){
return text.split('\n').map(line=>'<span class="code-line">'+_escHtml(line)+'</span>').join('\n');
}
function _highlightJSON(text){
try{
const pretty=JSON.stringify(JSON.parse(text),null,2);
return pretty.split('\n').map(raw=>{
let line=_escHtml(raw);
// Highlight keys
line=line.replace(/^(\s*)(&quot;)([\w\s.\/\-_@:]+)(&quot;)(\s*:)/,'$1<span class="hl-key">$2$3$4</span><span class="hl-value">$5</span>');
// Highlight string values (after colon)
line=line.replace(/(:\s*)(&quot;[^&]*?&quot;)/g,'$1<span class="hl-string">$2</span>');
// Highlight numbers
line=line.replace(/:\s*(\d+\.?\d*)/g,': <span class="hl-number">$1</span>');
// Highlight booleans / null
line=line.replace(/:\s*(true|false|null)/g,': <span class="hl-bool">$1</span>');
return '<span class="code-line">'+line+'</span>';
}).join('\n');
}catch(e){
return _highlightWithLineNumbers(text);
}
}
// ── Workspace file search (server-side recursive) ──
let _wsSearchTimer=null;
function filterWsFiles(){
// Debounce: wait 300ms after last keystroke
clearTimeout(_wsSearchTimer);
_wsSearchTimer=setTimeout(_doWsSearch,300);
}
async function _doWsSearch(){
const input=$('wsSearchInput');
const clearBtn=$('wsSearchClear');
const tree=$('fileTree');
if(!input||!tree)return;
const query=input.value.trim().toLowerCase();
if(clearBtn)clearBtn.classList.toggle('visible',query.length>0);
// Remove any stale "no results" message
const oldNoRes=tree.querySelector('.ws-no-results');
if(oldNoRes)oldNoRes.remove();
// If empty query, restore original file tree
if(!query){
if(typeof renderFileTree==='function')renderFileTree();
return;
}
// Not searchable without a workspace
if(!S.session||!S.session.workspace)return;
// Show loading indicator
tree.innerHTML='<div class="ws-no-results" style="opacity:.5">Suche...</div>';
try{
// Ask server to search recursively
const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=.&search=${encodeURIComponent(query)}`);
const results=data.entries||[];
if(!results.length){
tree.innerHTML='<div class="ws-no-results">Keine Dateien gefunden</div>';
function _restoreExpandedDirs() {
const key = _wsExpandKey();
if (!key) {
S._expandedDirs = /* @__PURE__ */ new Set();
return;
}
// Render flat result list with path info
tree.innerHTML='';
for(const item of results){
const el=document.createElement('div');
el.className='file-item';
el.style.paddingLeft='10px';
const iconEl=document.createElement('span');
iconEl.className='file-icon';
iconEl.innerHTML=fileIcon(item.name,item.type);
el.appendChild(iconEl);
const nameEl=document.createElement('span');
nameEl.className='file-name';
nameEl.textContent=item.name;
el.appendChild(nameEl);
// Show relative path as hint
if(item.path){
const pathEl=document.createElement('span');
pathEl.className='file-size';
pathEl.style.opacity='.4';
const dir=item.path.substring(0,item.path.lastIndexOf('/'));
pathEl.textContent=dir||'.';
el.appendChild(pathEl);
}
if(item.type==='dir'){
el.onclick=()=>{clearWsSearch();loadDir(item.path);};
}else{
el.onclick=()=>openFile(item.path);
}
tree.appendChild(el);
try {
const raw = localStorage.getItem(key);
S._expandedDirs = raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
} catch (e) {
S._expandedDirs = /* @__PURE__ */ new Set();
}
}catch(e){
// Fallback: client-side filter on currently visible items
tree.innerHTML='';
const allItems=S.entries||[];
const matches=allItems.filter(it=>it.name.toLowerCase().includes(query));
if(!matches.length){
tree.innerHTML='<div class="ws-no-results">Keine Dateien gefunden</div>';
}else{
for(const item of matches){
const el=document.createElement('div');
el.className='file-item';el.style.paddingLeft='10px';
el.innerHTML='<span class="file-icon">'+fileIcon(item.name,item.type)+'</span><span class="file-name">'+item.name+'</span>';
el.onclick=item.type==='dir'?()=>{clearWsSearch();loadDir(item.path);}:()=>openFile(item.path);
tree.appendChild(el);
}
async function loadDir(path) {
if (!S.session) return;
try {
if (!path || path === ".") {
S._dirCache = {};
_restoreExpandedDirs();
}
S.currentDir = path || ".";
const data = await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
S.entries = data.entries || [];
renderBreadcrumb();
renderFileTree();
if (!path || path === ".") {
for (const dirPath of S._expandedDirs || []) {
if (!S._dirCache[dirPath]) {
try {
const dc = await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(dirPath)}`);
S._dirCache[dirPath] = dc.entries || [];
} catch (e2) {
S._dirCache[dirPath] = [];
}
}
}
if (S._expandedDirs && S._expandedDirs.size > 0) renderFileTree();
}
if (typeof clearPreview === "function") {
if (typeof _previewDirty !== "undefined" && _previewDirty) {
showConfirmDialog({ title: t("unsaved_confirm"), message: "", confirmLabel: "Discard", danger: true, focusCancel: true }).then((ok) => {
if (ok) clearPreview();
});
} else {
clearPreview();
}
}
if (!path || path === ".") _refreshGitBadge();
} catch (e) {
console.warn("loadDir", e);
}
}
async function _refreshGitBadge() {
const badge = $("gitBadge");
if (!badge || !S.session) return;
try {
const data = await api(`/api/git-info?session_id=${encodeURIComponent(S.session.session_id)}`);
if (data.git && data.git.is_git) {
const g = data.git;
let text = g.branch || "git";
if ((g.dirty ?? 0) > 0) text += ` \xB7 ${g.dirty}\u2206`;
if ((g.behind ?? 0) > 0) text += ` \u2193${g.behind}`;
if ((g.ahead ?? 0) > 0) text += ` \u2191${g.ahead}`;
badge.textContent = text;
badge.className = "git-badge" + ((g.dirty ?? 0) > 0 ? " dirty" : "");
badge.style.display = "";
} else {
badge.style.display = "none";
badge.textContent = "";
}
} catch (e) {
if (badge) badge.style.display = "none";
}
}
function navigateUp() {
if (!S.session || S.currentDir === ".") return;
const parts = S.currentDir.split("/");
parts.pop();
loadDir(parts.length ? parts.join("/") : ".");
}
let _previewCurrentPath = "";
let _previewCurrentMode = "";
let _previewDirty = false;
let _previewRawContent = "";
function showPreview(mode) {
const codeEl = $("previewCode");
const imgWrap = $("previewImgWrap");
const mdEl = $("previewMd");
const editEl = $("previewEditArea");
const badge = $("previewBadge");
if (codeEl) codeEl.style.display = mode === "code" ? "" : "none";
if (imgWrap) imgWrap.style.display = mode === "image" ? "" : "none";
if (mdEl) mdEl.style.display = mode === "md" ? "" : "none";
if (editEl) editEl.style.display = "none";
if (badge) {
badge.className = "preview-badge " + mode;
const pathText = $("previewPathText");
badge.textContent = mode === "image" ? "image" : mode === "md" ? "md" : fileExt(pathText?.textContent || "");
}
_previewCurrentMode = mode;
_previewDirty = false;
updateEditBtn();
}
function updateEditBtn() {
const btn = $("btnEditFile");
if (!btn) return;
const editable = _previewCurrentMode === "code" || _previewCurrentMode === "md";
btn.style.display = editable ? "" : "none";
const editing = $("previewEditArea")?.style.display !== "none";
btn.innerHTML = editing ? `&#128190; ${t("save")}` : `&#9998; ${t("edit")}`;
btn.title = editing ? t("save_title") : t("edit_title");
btn.style.color = editing ? "var(--blue)" : "";
if (_previewDirty) btn.innerHTML = "&#128190; Save*";
}
async function toggleEditMode() {
const editEl = $("previewEditArea");
const editing = editEl && editEl.style.display !== "none";
if (editing) {
if (!S.session || !_previewCurrentPath) return;
const content = editEl.value;
try {
await api("/api/file/save", { method: "POST", body: JSON.stringify({ session_id: S.session.session_id, path: _previewCurrentPath, content }) });
_previewDirty = false;
const codeEl = $("previewCode");
const mdEl = $("previewMd");
if (_previewCurrentMode === "code" && codeEl) codeEl.textContent = content;
else if (mdEl) {
mdEl.innerHTML = renderMd(content);
requestAnimationFrame(() => {
if (typeof renderKatexBlocks === "function") renderKatexBlocks();
});
}
if (editEl) editEl.style.display = "none";
if (codeEl && _previewCurrentMode === "code") codeEl.style.display = "";
else if (mdEl) mdEl.style.display = "";
showToast(t("saved"));
} catch (e) {
setStatus(t("save_failed") + (e instanceof Error ? e.message : String(e)));
}
} else {
const currentText = _previewCurrentMode === "code" ? $("previewCode")?.textContent || "" : _previewRawContent || "";
if (editEl) {
editEl.value = currentText;
editEl.style.display = "";
}
const codeEl = $("previewCode");
const mdEl = $("previewMd");
if (codeEl && _previewCurrentMode === "code") codeEl.style.display = "none";
else if (mdEl) mdEl.style.display = "none";
editEl.onkeydown = (e) => {
if (e.key === "Escape") {
e.preventDefault();
cancelEditMode();
}
};
}
updateEditBtn();
}
function cancelEditMode() {
const editEl = $("previewEditArea");
if (editEl) {
editEl.style.display = "none";
editEl.onkeydown = null;
}
const codeEl = $("previewCode");
const mdEl = $("previewMd");
if (codeEl && _previewCurrentMode === "code") codeEl.style.display = "";
else if (mdEl) mdEl.style.display = "";
_previewDirty = false;
updateEditBtn();
}
async function openFile(path) {
if (!S.session) return;
const ext = fileExt(path);
if (DOWNLOAD_EXTS.has(ext)) {
downloadFile(path);
return;
}
const previewPathText = $("previewPathText");
const previewArea = $("previewArea");
const fileTree = $("fileTree");
const wsSearch = $("wsSearchWrap");
if (previewPathText) previewPathText.textContent = path;
previewArea?.classList.add("visible");
if (fileTree) fileTree.style.display = "none";
if (wsSearch) wsSearch.style.display = "none";
_previewCurrentPath = path;
renderFileBreadcrumb(path);
if (IMAGE_EXTS.has(ext)) {
showPreview("image");
const url = `api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`;
const previewImg = $("previewImg");
if (previewImg) {
previewImg.alt = path;
previewImg.src = url;
previewImg.onerror = () => setStatus(t("image_load_failed"));
}
} else if (MD_EXTS.has(ext)) {
try {
const data = await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
showPreview("md");
_previewRawContent = data.content || "";
const mdEl = $("previewMd");
if (mdEl) mdEl.innerHTML = renderMd(data.content || "");
requestAnimationFrame(() => {
if (typeof renderKatexBlocks === "function") renderKatexBlocks();
});
} catch (e) {
setStatus(t("file_open_failed"));
}
} else {
try {
const data = await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
if (data.binary) {
downloadFile(path);
return;
}
showPreview("code");
const content = data.content || "";
const codeEl = $("previewCode");
if (["yml", "yaml"].includes(ext)) {
if (codeEl) {
codeEl.className = "preview-code hl-yaml";
codeEl.innerHTML = typeof highlightYAML === "function" ? highlightYAML(content) : _highlightWithLineNumbers(content);
}
} else if (ext === "json") {
if (codeEl) {
codeEl.className = "preview-code hl-json";
codeEl.innerHTML = _highlightJSON(content);
}
} else if (["py", "js", "ts", "sh", "bash", "zsh", "rb", "go", "rs", "java", "c", "cpp", "h", "css", "scss", "html", "xml", "sql", "r", "lua", "pl", "php", "swift", "kt", "dart"].includes(ext)) {
if (codeEl) {
codeEl.className = "preview-code";
codeEl.textContent = content;
requestAnimationFrame(() => {
if (typeof highlightCode === "function") highlightCode();
});
}
} else {
if (codeEl) {
codeEl.className = "preview-code hl-text";
codeEl.innerHTML = _highlightWithLineNumbers(content);
}
}
} catch (e) {
downloadFile(path);
}
}
}
}
// Toggle workspace search visibility
function toggleWsSearch(){
const wrap=$('wsSearchWrap');
if(!wrap)return;
// Show/hide the search bar
wrap.style.display=wrap.style.display==='none'?'flex':'none';
// Focus input when showing
setTimeout(()=>{if(wrap.style.display!=='none'){const inp=$('wsSearchInput');if(inp)inp.focus();}},50);
}
function clearWsSearch(){
const input=$('wsSearchInput');
if(input)input.value='';
const clearBtn=$('wsSearchClear');
if(clearBtn)clearBtn.classList.remove('visible');
if(typeof renderFileTree==='function')renderFileTree();
}
function downloadFile(path) {
if (!S.session) return;
const url = `api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&download=1`;
const filename = path.split("/").pop() || path;
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => document.body.removeChild(a), 100);
showToast(t("downloading", filename), 2e3);
}
function renderFileBreadcrumb(filePath) {
const bar = $("breadcrumbBar");
if (!bar) return;
bar.style.display = "flex";
const upBtn = $("btnUpDir");
if (upBtn) upBtn.style.display = "";
bar.innerHTML = "";
const root = document.createElement("span");
root.className = "breadcrumb-seg breadcrumb-link";
root.textContent = "~";
root.onclick = () => {
clearPreview();
loadDir(".");
};
bar.appendChild(root);
const parts = filePath.split("/");
let accumulated = "";
for (let i = 0; i < parts.length; i++) {
const sep = document.createElement("span");
sep.className = "breadcrumb-sep";
sep.textContent = "/";
bar.appendChild(sep);
accumulated += (accumulated ? "/" : "") + parts[i];
const seg = document.createElement("span");
seg.textContent = parts[i];
if (i < parts.length - 1) {
seg.className = "breadcrumb-seg breadcrumb-link";
const target = accumulated;
seg.onclick = () => {
clearPreview();
loadDir(target);
};
} else {
seg.className = "breadcrumb-seg breadcrumb-current";
}
bar.appendChild(seg);
}
}
function _escHtml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function _highlightWithLineNumbers(text) {
return text.split("\n").map((line) => '<span class="code-line">' + _escHtml(line) + "</span>").join("\n");
}
function _highlightJSON(text) {
try {
const pretty = JSON.stringify(JSON.parse(text), null, 2);
return pretty.split("\n").map((raw) => {
let line = _escHtml(raw);
line = line.replace(/^(\s*)(&quot;)([\w\s.\/\-_@:]+)(&quot;)(\s*:)/, '$1<span class="hl-key">$2$3$4</span><span class="hl-value">$5</span>');
line = line.replace(/(:\s*)(&quot;[^&]*?&quot;)/g, '$1<span class="hl-string">$2</span>');
line = line.replace(/:\s*(\d+\.?\d*)/g, ': <span class="hl-number">$1</span>');
line = line.replace(/:\s*(true|false|null)/g, ': <span class="hl-bool">$1</span>');
return '<span class="code-line">' + line + "</span>";
}).join("\n");
} catch {
return _highlightWithLineNumbers(text);
}
}
let _wsSearchTimer = null;
function filterWsFiles() {
clearTimeout(_wsSearchTimer ?? void 0);
_wsSearchTimer = setTimeout(_doWsSearch, 300);
}
async function _doWsSearch() {
const input = $("wsSearchInput");
const clearBtn = $("wsSearchClear");
const tree = $("fileTree");
if (!input || !tree) return;
const query = input.value.trim().toLowerCase();
if (clearBtn) clearBtn.classList.toggle("visible", query.length > 0);
const oldNoRes = tree.querySelector(".ws-no-results");
if (oldNoRes) oldNoRes.remove();
if (!query) {
if (typeof renderFileTree === "function") renderFileTree();
return;
}
if (!S.session || !S.session.workspace) return;
tree.innerHTML = '<div class="ws-no-results" style="opacity:.5">Suche...</div>';
try {
const data = await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=.&search=${encodeURIComponent(query)}`);
const results = data.entries || [];
if (!results.length) {
tree.innerHTML = '<div class="ws-no-results">Keine Dateien gefunden</div>';
return;
}
tree.innerHTML = "";
for (const item of results) {
const el = document.createElement("div");
el.className = "file-item";
el.style.paddingLeft = "10px";
const iconEl = document.createElement("span");
iconEl.className = "file-icon";
iconEl.innerHTML = fileIcon(item.name, item.type);
el.appendChild(iconEl);
const nameEl = document.createElement("span");
nameEl.className = "file-name";
nameEl.textContent = item.name;
el.appendChild(nameEl);
if (item.path) {
const pathEl = document.createElement("span");
pathEl.className = "file-size";
pathEl.style.opacity = ".4";
const dir = item.path.substring(0, item.path.lastIndexOf("/"));
pathEl.textContent = dir || ".";
el.appendChild(pathEl);
}
if (item.type === "dir") el.onclick = () => {
clearWsSearch();
loadDir(item.path);
};
else el.onclick = () => openFile(item.path);
tree.appendChild(el);
}
} catch {
tree.innerHTML = "";
const allItems = S.entries || [];
const matches = allItems.filter((it) => it.name.toLowerCase().includes(query));
if (!matches.length) tree.innerHTML = '<div class="ws-no-results">Keine Dateien gefunden</div>';
else {
for (const item of matches) {
const el = document.createElement("div");
el.className = "file-item";
el.style.paddingLeft = "10px";
el.innerHTML = '<span class="file-icon">' + fileIcon(item.name, item.type) + '</span><span class="file-name">' + item.name + "</span>";
el.onclick = item.type === "dir" ? () => {
clearWsSearch();
loadDir(item.path);
} : () => openFile(item.path);
tree.appendChild(el);
}
}
}
}
function toggleWsSearch() {
const wrap = $("wsSearchWrap");
if (!wrap) return;
wrap.style.display = wrap.style.display === "none" ? "flex" : "none";
setTimeout(() => {
if (wrap.style.display !== "none") {
const inp = $("wsSearchInput");
if (inp) inp.focus();
}
}, 50);
}
function clearWsSearch() {
const input = $("wsSearchInput");
if (input) input.value = "";
const clearBtn = $("wsSearchClear");
if (clearBtn) clearBtn.classList.remove("visible");
if (typeof renderFileTree === "function") renderFileTree();
}
function refreshWorkspace() {
loadDir(".");
}
// ── Export to window for cross-module access ──
window.loadDir = loadDir;
window.renderFileTree = renderFileTree;
window.toggleWsSearch = toggleWsSearch;
window.clearWsSearch = clearWsSearch;
window.openFile = openFile;
window.showPreview = showPreview;
window.refreshWorkspace = refreshWorkspace;
})();
//# sourceMappingURL=workspace.js.map