From: Codebuddy Date: Sat, 15 Aug 2026 09:20:42 +0000 (+0800) Subject: feat: 首页常驻左侧技能面板(查看/上传/更新/编辑/删除/加载) X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=c5d108a0a34a79c5d70c8f558b49ae7c3fc8c110;p=codebuddy-web.git feat: 首页常驻左侧技能面板(查看/上传/更新/编辑/删除/加载) - 列表页改为左 300px 技能侧边栏 + 右任务网格布局 - 侧边栏常驻展示已加载技能,支持上传新增、zip 覆盖更新、内联编辑 SKILL.md、删除、加载到运行中会话 - 移除列表页右上角冗余的弹窗按钮;弹窗保留给终端页 - 侧边栏随列表页自动刷新(编辑/加载面板打开时跳过以免清掉输入) --- diff --git a/frontend/index.html b/frontend/index.html index 5da59ca..55b8a46 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -101,6 +101,29 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over .picker-header h2 { font-size: 22px; color: #fff; } .picker-header h2 small { color: #888; font-size: 14px; font-weight: normal; margin-left: 10px; } .picker-body { flex: 1; overflow: auto; padding: 28px; } +.picker-main { display: flex; flex: 1; min-height: 0; } +#skills-sidebar { + width: 300px; flex-shrink: 0; background: #16213e; border-right: 1px solid rgba(255,255,255,0.08); + display: flex; flex-direction: column; padding: 14px; gap: 12px; overflow: auto; +} +#skills-sidebar .ss-head { color: #74c0fc; font-size: 15px; font-weight: 600; } +#skills-sidebar .ss-upload { display: flex; gap: 8px; } +#skills-sidebar .ss-upload input[type=file] { flex: 1; min-width: 0; color: #ccc; font-size: 12px; } +.ss-btn { padding: 6px 12px; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; background: rgba(255,255,255,0.06); color: #e0e0e0; font-size: 13px; cursor: pointer; } +.ss-btn:hover { background: rgba(255,255,255,0.12); } +.ss-btn.ss-primary { background: #4ecca3; color: #1a1a2e; border-color: #4ecca3; font-weight: 600; } +.ss-btn.ss-cancel { background: transparent; } +.ss-btn.ss-danger { background: rgba(255,107,107,0.15); color: #ff6b6b; border-color: rgba(255,107,107,0.3); } +.ss-card { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 10px 12px; } +.ss-card .ss-name { color: #e0e0e0; font-size: 14px; font-weight: 600; } +.ss-card .ss-desc { color: #aaa; font-size: 12px; margin: 4px 0 8px; line-height: 1.4; } +.ss-card .ss-actions { display: flex; gap: 8px; } +.ss-card .ss-actions .ss-btn { flex: 1; } +.ss-row-end { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; } +#skills-sidebar textarea { width: 100%; box-sizing: border-box; background: #0f0f1e; color: #e0e0e0; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; padding: 10px; font-family: 'Cascadia Code', Menlo, monospace; font-size: 12px; line-height: 1.5; resize: vertical; } +.ss-edit-title { color: #e0e0e0; font-size: 13px; margin-bottom: 6px; } +#skills-sidebar label.ss-session { display: flex; gap: 8px; align-items: center; font-size: 13px; color: #e0e0e0; margin: 4px 0; } +#skills-sidebar .ss-empty { color: #777; font-size: 13px; } .task-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; max-width: 1200px; margin: 0 auto; @@ -187,12 +210,38 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over

任务清单

-
-
-
+
+ +
+
+
@@ -387,6 +436,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over async function showPicker() { show('picker'); await renderTasks(); + await renderSkillsSidebar(); } // 列表页自动刷新:进入时启动,离开/退出时停止 @@ -402,7 +452,10 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over async function refreshTasks() { if (pickerRefreshing) return; pickerRefreshing = true; - try { await renderTasks(); } catch (e) { console.warn('refreshTasks failed', e); } + try { + await renderTasks(); + await renderSkillsSidebar(); + } catch (e) { console.warn('refreshTasks failed', e); } finally { pickerRefreshing = false; } } @@ -839,7 +892,6 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over if (term && term.focus) term.focus(); } document.getElementById('btn-skills').onclick = openSkillsModal; - document.getElementById('btn-skills-picker').onclick = openSkillsModal; document.getElementById('skills-close').onclick = closeSkillsModal; skillsModal.addEventListener('click', (e) => { if (e.target === skillsModal) closeSkillsModal(); }); document.addEventListener('keydown', (e) => { @@ -1010,6 +1062,180 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over loadSkills(); }; + // ========== 首页技能侧边栏(常驻面板) ========== + const ssList = document.getElementById('ss-list'); + const ssFile = document.getElementById('ss-file'); + const ssEdit = document.getElementById('ss-edit'); + const ssEditArea = document.getElementById('ss-edit-area'); + const ssEditName = document.getElementById('ss-edit-name'); + const ssLoad = document.getElementById('ss-load'); + const ssLoadList = document.getElementById('ss-load-list'); + const ssLoadName = document.getElementById('ss-load-name'); + const ssUpdateFile = document.getElementById('ss-update-file'); + let ssEditTarget = null; + let ssLoadTarget = null; + let ssUpdateTarget = null; + + async function renderSkillsSidebar() { + // 正在编辑/选择加载时,跳过自动刷新以免清掉用户输入 + if (ssEdit.style.display === 'block' || ssLoad.style.display === 'block') return; + try { + const res = await fetch(API + '/api/skills', { headers: authHeaders() }); + if (!res.ok) return; + const data = await res.json(); + const skills = data.skills || []; + if (!skills.length) { + ssList.innerHTML = '
暂无自定义 skill。上传一个 zip 开始。
'; + return; + } + ssList.innerHTML = skills.map(s => + '
' + + '
' + escHtml(s.name) + + (s.has_skill_md ? '' : ' (缺 SKILL.md)') + '
' + + (s.description ? '
' + escHtml(s.description) + '
' : '
') + + '
' + + '' + + '' + + '' + + '' + + '
' + ).join(''); + ssList.querySelectorAll('button[data-act]').forEach(btn => { + btn.onclick = () => { + const act = btn.dataset.act, n = btn.dataset.name; + if (act === 'edit') ssEditSkill(n); + else if (act === 'update') ssUpdateSkill(n); + else if (act === 'del') ssDeleteSkill(n); + else if (act === 'load') ssLoadSkill(n); + }; + }); + } catch (e) { console.warn('renderSkillsSidebar failed', e); } + } + + document.getElementById('ss-upload').onclick = async () => { + const f = ssFile.files && ssFile.files[0]; + if (!f) { alert('请先选择一个 .zip 文件'); return; } + try { + const b64 = await readFileAsBase64(f); + const res = await fetch(API + '/api/skills', { + method: 'POST', + headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()), + body: JSON.stringify({ data: b64 }) + }); + const d = await res.json(); + if (!res.ok || !d.ok) throw new Error(d.error || ('HTTP ' + res.status)); + ssFile.value = ''; + renderSkillsSidebar(); + } catch (e) { alert('上传失败: ' + e.message); } + }; + + // 更新:用新 zip 覆盖该 skill 目录(name 锁定为原目录名) + function ssUpdateSkill(name) { + ssUpdateTarget = name; + ssUpdateFile.click(); + } + ssUpdateFile.onchange = async () => { + const f = ssUpdateFile.files && ssUpdateFile.files[0]; + if (!f) { ssUpdateTarget = null; return; } + const target = ssUpdateTarget; ssUpdateTarget = null; + try { + const b64 = await readFileAsBase64(f); + const res = await fetch(API + '/api/skills', { + method: 'POST', + headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()), + body: JSON.stringify({ data: b64, name: target }) + }); + const d = await res.json(); + if (!res.ok || !d.ok) throw new Error(d.error || ('HTTP ' + res.status)); + ssUpdateFile.value = ''; + renderSkillsSidebar(); + } catch (e) { alert('更新失败: ' + e.message); } + }; + + async function ssEditSkill(name) { + try { + const res = await fetch(API + '/api/skills', { headers: authHeaders() }); + const data = await res.json(); + const s = (data.skills || []).find(x => x.dir_name === name); + if (!s) throw new Error('未找到 skill'); + const res2 = await fetch(API + '/api/skills/' + encodeURIComponent(name) + '/content', { headers: authHeaders() }); + const d2 = await res2.json(); + if (!res2.ok || !d2.ok) throw new Error(d2.error || '读取失败'); + ssEditName.textContent = s.name || name; + ssEditTarget = name; + ssEditArea.value = d2.content; + ssLoad.style.display = 'none'; + ssEdit.style.display = 'block'; + } catch (e) { alert('打开编辑失败: ' + e.message); } + } + document.getElementById('ss-edit-cancel').onclick = () => { ssEdit.style.display = 'none'; ssEditTarget = null; }; + document.getElementById('ss-edit-save').onclick = async () => { + if (!ssEditTarget) return; + const name = ssEditTarget; + try { + const res = await fetch(API + '/api/skills/' + encodeURIComponent(name), { + method: 'PUT', headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()), + body: JSON.stringify({ content: ssEditArea.value }) + }); + const d = await res.json(); + if (!res.ok || !d.ok) throw new Error(d.error || ('HTTP ' + res.status)); + ssEdit.style.display = 'none'; ssEditTarget = null; + renderSkillsSidebar(); + } catch (e) { alert('保存失败: ' + e.message); } + }; + + async function ssDeleteSkill(name) { + if (!confirm('确认删除 skill: ' + name + '?该目录将被永久移除。')) return; + try { + const res = await fetch(API + '/api/skills/' + encodeURIComponent(name), { method: 'DELETE', headers: authHeaders() }); + const d = await res.json(); + if (!res.ok || !d.ok) throw new Error(d.error || ('HTTP ' + res.status)); + renderSkillsSidebar(); + } catch (e) { alert('删除失败: ' + e.message); } + } + + async function ssLoadSkill(name) { + try { + const res = await fetch(API + '/api/tasks', { headers: authHeaders() }); + const data = await res.json(); + const tasks = data.tasks || []; + const living = tasks.filter(t => ['connected', 'background', 'background_idle'].includes(t.live_status)); + if (!living.length) { + alert('当前没有运行中的会话,技能已全局就位,所有会话下次打开时将自动发现此技能。'); + return; + } + ssLoadTarget = name; + ssLoadName.textContent = name; + ssLoadList.innerHTML = living.map(t => + '' + ).join('') + + ''; + ssEdit.style.display = 'none'; + ssLoad.style.display = 'block'; + } catch (e) { alert('加载失败: ' + e.message); } + } + document.getElementById('ss-load-cancel').onclick = () => { ssLoad.style.display = 'none'; ssLoadTarget = null; }; + document.getElementById('ss-load-confirm').onclick = async () => { + if (!ssLoadTarget) return; + const name = ssLoadTarget; + const all = document.getElementById('ss-load-all').checked; + const cbs = Array.from(ssLoadList.querySelectorAll('.ss-load-cb')); + const ids = all ? cbs.map(c => c.value) : cbs.filter(c => c.checked).map(c => c.value); + if (!ids.length) { alert('请至少选择一个会话'); return; } + let ok = 0, fail = 0; + for (const tid of ids) { + try { + const res = await fetch(API + '/api/tasks/' + tid + '/skills/' + encodeURIComponent(name) + '/notify', { method: 'POST', headers: authHeaders() }); + const d = await res.json(); + if (res.ok && d.ok && d.notified) ok++; else fail++; + } catch (e) { fail++; } + } + alert('已向 ' + ok + ' 个会话注入热加载通知' + (fail ? (',' + fail + ' 个失败') : '') + '。'); + ssLoad.style.display = 'none'; ssLoadTarget = null; + renderSkillsSidebar(); + }; + // ✕ 清空 pasteClear.onclick = () => { pasteInput.value = '';