1338 lines
67 KiB
HTML
1338 lines
67 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>GitLink CLI · 子任务一 交互式展示</title>
|
||
<style>
|
||
/* ═══════════════════════════════════════════════════════════
|
||
CSS Variables (Light Theme)
|
||
═══════════════════════════════════════════════════════════ */
|
||
:root {
|
||
--bg-root: #eef0f4;
|
||
--bg-sidebar: #f7f8fa;
|
||
--bg-main: #fcfcfd;
|
||
--bg-terminal: #ffffff;
|
||
--bg-input: #fafbfc;
|
||
--bg-card: #f0f2f5;
|
||
--bg-dropdown: #ffffff;
|
||
--bg-hover: #e4e7ec;
|
||
--border: #d4d7de;
|
||
--border-accent: #b0b8c8;
|
||
--text: #3b3e45;
|
||
--text-dim: #767b86;
|
||
--text-bright: #15171c;
|
||
--accent: #1a6fd6;
|
||
--accent-glow: rgba(26,111,214,0.18);
|
||
--green: #1a7f37;
|
||
--green-dim: #1f883d;
|
||
--red: #cf222e;
|
||
--red-dim: #a40e26;
|
||
--yellow: #9a6700;
|
||
--purple: #8250df;
|
||
--cyan: #0a7e8c;
|
||
--font-mono: 'Cascadia Code','Fira Code','JetBrains Mono','Consolas','Courier New',monospace;
|
||
--font-sans: 'Segoe UI','Inter',system-ui,-apple-system,sans-serif;
|
||
--radius: 6px;
|
||
--radius-sm: 4px;
|
||
--shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.12);
|
||
}
|
||
|
||
* { margin:0; padding:0; box-sizing:border-box; }
|
||
html, body { height:100%; overflow:hidden; }
|
||
body {
|
||
font-family: var(--font-sans);
|
||
background: var(--bg-root);
|
||
color: var(--text);
|
||
display:flex;
|
||
flex-direction:column;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Top Bar
|
||
═══════════════════════════════════════════════════════════ */
|
||
#topbar {
|
||
display:flex; align-items:center; justify-content:space-between;
|
||
height:44px; padding:0 18px;
|
||
background: #ffffff;
|
||
border-bottom:1px solid var(--border);
|
||
flex-shrink:0;
|
||
user-select:none;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||
}
|
||
#topbar .title {
|
||
font-size:14px; font-weight:600; color:var(--text-bright);
|
||
display:flex; align-items:center; gap:8px;
|
||
}
|
||
#topbar .title .dot {
|
||
width:8px; height:8px; border-radius:50%;
|
||
background:var(--green);
|
||
box-shadow: 0 0 6px rgba(26,127,55,0.3);
|
||
}
|
||
#topbar .actions { display:flex; gap:8px; align-items:center; }
|
||
#topbar .actions button {
|
||
background: var(--bg-card); border:1px solid var(--border);
|
||
color: var(--text); padding:4px 14px; border-radius:var(--radius-sm);
|
||
cursor:pointer; font-size:12px; transition: all .15s;
|
||
}
|
||
#topbar .actions button:hover {
|
||
background: var(--bg-hover); border-color: var(--border-accent);
|
||
}
|
||
#topbar .status { font-size:11px; color:var(--text-dim); display:flex; align-items:center; gap:6px; }
|
||
#topbar .status .indicator { width:6px; height:6px; border-radius:50%; }
|
||
#topbar .status .indicator.online { background:var(--green); box-shadow:0 0 6px rgba(26,127,55,0.4); }
|
||
#topbar .status .indicator.offline { background:var(--red); }
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Layout
|
||
═══════════════════════════════════════════════════════════ */
|
||
#app {
|
||
display:flex; flex:1; overflow:hidden;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Sidebar
|
||
═══════════════════════════════════════════════════════════ */
|
||
#sidebar {
|
||
width:340px; min-width:280px;
|
||
background: var(--bg-sidebar);
|
||
border-right:1px solid var(--border);
|
||
display:flex; flex-direction:column;
|
||
overflow-y:auto; overflow-x:hidden;
|
||
}
|
||
#sidebar::-webkit-scrollbar { width:5px; }
|
||
#sidebar::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
|
||
#sidebar .sidebar-header {
|
||
padding:10px 14px; font-size:12px; font-weight:700;
|
||
color:var(--text-dim); text-transform:uppercase; letter-spacing:.06em;
|
||
border-bottom:1px solid var(--border);
|
||
flex-shrink:0;
|
||
background: #fafbfc;
|
||
}
|
||
.sidebar-section {
|
||
flex-shrink:0;
|
||
}
|
||
.sidebar-section + .sidebar-section {
|
||
border-top: 2px solid var(--border);
|
||
}
|
||
.sidebar-section .skills-header {
|
||
background: #fdf8ff;
|
||
}
|
||
/* ── Section collapse (top-level toggle) ── */
|
||
.sidebar-section .sidebar-header {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.sidebar-section .sidebar-header:hover {
|
||
background: #eef1f6;
|
||
}
|
||
.sidebar-section .skills-header:hover {
|
||
background: #f5edfa;
|
||
}
|
||
.sidebar-section .modules {
|
||
display: none;
|
||
}
|
||
.sidebar-section.open .modules {
|
||
display: block;
|
||
}
|
||
.sidebar-section .sidebar-header .section-arrow {
|
||
font-size: 10px;
|
||
color: var(--text-dim);
|
||
transition: transform 0.2s;
|
||
margin-right: 2px;
|
||
}
|
||
.sidebar-section.open .sidebar-header .section-arrow {
|
||
transform: rotate(90deg);
|
||
}
|
||
.mode-toggle {
|
||
display:flex; gap:0; margin-top:6px;
|
||
}
|
||
.mode-toggle button {
|
||
flex:1; padding:5px 8px; border:1px solid var(--border);
|
||
background:#fff; color:var(--text-dim); cursor:pointer;
|
||
font-size:11px; font-weight:500; transition: all .12s;
|
||
}
|
||
.mode-toggle button:first-child { border-radius:var(--radius-sm) 0 0 var(--radius-sm); }
|
||
.mode-toggle button:last-child { border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
|
||
.mode-toggle button.active {
|
||
background:var(--accent); color:#fff; border-color:var(--accent);
|
||
z-index:1;
|
||
}
|
||
.mode-toggle button:hover:not(.active) {
|
||
background:var(--bg-hover); border-color:var(--border-accent);
|
||
}
|
||
/* ── Agent selector buttons ── */
|
||
.agent-selector {
|
||
display:flex; gap:0; margin-top:6px;
|
||
}
|
||
.agent-selector button {
|
||
flex:1; padding:5px 8px; border:1px solid var(--border);
|
||
background:#fff; color:var(--text-dim); cursor:pointer;
|
||
font-size:11px; font-weight:500; transition: all .12s;
|
||
}
|
||
.agent-selector button:first-child { border-radius:var(--radius-sm) 0 0 var(--radius-sm); }
|
||
.agent-selector button:last-child { border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
|
||
.agent-selector button:hover:not(.active) {
|
||
background:var(--bg-hover); border-color:var(--border-accent);
|
||
}
|
||
.agent-selector button.active { color:#fff; z-index:1; }
|
||
.agent-selector button.agent-claude.active { background:var(--purple); border-color:var(--purple); }
|
||
.agent-selector button.agent-codex.active { background:#0ea5e9; border-color:#0ea5e9; }
|
||
.agent-selector button.agent-zcode.active { background:#f59e0b; border-color:#f59e0b; }
|
||
#sidebar .modules {
|
||
padding:4px 0;
|
||
}
|
||
|
||
/* ── Module accordion ── */
|
||
.module {
|
||
border-bottom:1px solid rgba(212,215,222,0.5);
|
||
}
|
||
.module-header {
|
||
display:flex; align-items:center; gap:10px;
|
||
padding:11px 18px; cursor:pointer;
|
||
font-size:13px; font-weight:500; color:var(--text-bright);
|
||
transition: background .12s;
|
||
user-select:none;
|
||
}
|
||
.module-header:hover { background: var(--bg-hover); }
|
||
.module-header .icon { font-size:15px; width:22px; text-align:center; flex-shrink:0; }
|
||
.module-header .name { flex:1; }
|
||
.module-header .arrow {
|
||
font-size:10px; color:var(--text-dim); transition: transform .2s;
|
||
}
|
||
.module.open .module-header .arrow { transform:rotate(90deg); }
|
||
.module.open .module-header {
|
||
background: var(--bg-hover);
|
||
border-left: 3px solid var(--accent);
|
||
padding-left: 15px;
|
||
}
|
||
|
||
.module-body {
|
||
display:none; padding:6px 18px 10px 18px;
|
||
}
|
||
.module.open .module-body { display:block; }
|
||
|
||
/* ── Command dropdown ── */
|
||
.cmd-select-wrap {
|
||
position:relative; margin-top:4px;
|
||
}
|
||
.cmd-select-wrap select {
|
||
width:100%; padding:8px 30px 8px 11px;
|
||
background: var(--bg-dropdown);
|
||
color: var(--text);
|
||
border:1px solid var(--border);
|
||
border-radius:var(--radius-sm);
|
||
font-size:12px; font-family:var(--font-sans);
|
||
cursor:pointer; outline:none;
|
||
appearance:none; -webkit-appearance:none;
|
||
transition: border-color .15s, box-shadow .15s;
|
||
}
|
||
.cmd-select-wrap select:hover { border-color:var(--border-accent); }
|
||
.cmd-select-wrap select:focus {
|
||
border-color:var(--accent);
|
||
box-shadow:0 0 0 3px var(--accent-glow);
|
||
}
|
||
.cmd-select-wrap::after {
|
||
content:'▾'; position:absolute; right:10px; top:50%; transform:translateY(-50%);
|
||
color:var(--text-dim); font-size:11px; pointer-events:none;
|
||
}
|
||
|
||
.cmd-preview {
|
||
margin-top:5px; padding:7px 10px;
|
||
background: #f6f8fa;
|
||
border:1px solid var(--border);
|
||
border-radius:var(--radius-sm);
|
||
font-family:var(--font-mono); font-size:11px;
|
||
color:var(--text-dim); white-space:pre-wrap; word-break:break-all;
|
||
max-height:48px; overflow:hidden;
|
||
line-height:1.45;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Main Terminal Area
|
||
═══════════════════════════════════════════════════════════ */
|
||
#main {
|
||
flex:1; display:flex; flex-direction:column;
|
||
background: var(--bg-main);
|
||
overflow:hidden;
|
||
min-width:0;
|
||
}
|
||
#terminal {
|
||
flex:1; overflow-y:auto; padding:18px;
|
||
font-family:var(--font-mono); font-size:13px;
|
||
line-height:1.6;
|
||
background: var(--bg-terminal);
|
||
display:flex; flex-direction:column;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
margin: 12px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
#terminal::-webkit-scrollbar { width:6px; }
|
||
#terminal::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
|
||
|
||
/* ── Terminal entries ── */
|
||
.entry { margin-bottom:1px; }
|
||
.entry.cmd-line { color:var(--cyan); font-weight:500; }
|
||
.entry.cmd-line .prompt { color:var(--green); user-select:none; font-weight:600; }
|
||
.entry.cmd-line .cmd-text { color: var(--text-bright); }
|
||
.entry.output { color:var(--text); white-space:pre-wrap; word-break:break-all; }
|
||
.entry.stderr { color:var(--red); white-space:pre-wrap; word-break:break-all; }
|
||
.entry.info { color:var(--text-dim); font-style:italic; }
|
||
.entry.success { color:var(--green); font-weight:500; }
|
||
.entry.error { color:var(--red); font-weight:500; }
|
||
.entry.link { color:var(--text); background:var(--bg-card); border-radius:var(--radius-sm); padding:6px 10px; font-size:13px; }
|
||
.entry.link a:hover { opacity:0.8; }
|
||
|
||
/* ── Input area ── */
|
||
#input-area {
|
||
display:flex; align-items:stretch;
|
||
border-top:2px solid var(--border);
|
||
background: #ffffff;
|
||
flex-shrink:0;
|
||
}
|
||
#input-area .prompt {
|
||
display:flex; align-items:center; padding:0 14px;
|
||
font-family:var(--font-mono); font-size:13px; font-weight:700;
|
||
color:var(--green); user-select:none;
|
||
background:#fafbfc;
|
||
}
|
||
#input-area textarea {
|
||
flex:1; padding:10px 14px;
|
||
background:#ffffff; color:var(--text-bright);
|
||
border:none; outline:none; resize:none;
|
||
font-family:var(--font-mono); font-size:13px;
|
||
line-height:1.55;
|
||
min-height:38px; max-height:200px;
|
||
overflow-y:auto;
|
||
}
|
||
#input-area textarea::placeholder { color:var(--text-dim); }
|
||
#btn-run {
|
||
padding:8px 22px;
|
||
background:var(--accent); color:#fff;
|
||
border:none; cursor:pointer;
|
||
font-size:12px; font-weight:600; letter-spacing:.03em;
|
||
transition: background .15s;
|
||
white-space:nowrap;
|
||
}
|
||
#btn-run:hover { background:#1460c0; }
|
||
#btn-run:active { background:#1050a8; }
|
||
#btn-run.running { background:var(--yellow); pointer-events:none; }
|
||
#btn-cancel {
|
||
padding:8px 16px; background:var(--red); color:#fff;
|
||
border:none; cursor:pointer; display:none;
|
||
font-size:12px; font-weight:600; letter-spacing:.03em;
|
||
transition: background .15s; white-space:nowrap;
|
||
}
|
||
#btn-cancel:hover { background:#a40e26; }
|
||
#btn-cancel.visible { display:inline-block; }
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Resizer
|
||
═══════════════════════════════════════════════════════════ */
|
||
#resizer {
|
||
width:4px; cursor:col-resize; background:transparent;
|
||
transition: background .2s; flex-shrink:0;
|
||
}
|
||
#resizer:hover, #resizer.active { background:var(--accent); opacity:0.5; }
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Toast
|
||
═══════════════════════════════════════════════════════════ */
|
||
#toast {
|
||
position:fixed; bottom:28px; right:28px;
|
||
padding:10px 20px; border-radius:var(--radius);
|
||
font-size:13px; color:#fff; z-index:1000;
|
||
pointer-events:none; opacity:0; transform:translateY(10px);
|
||
transition: all .25s;
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
#toast.show { opacity:1; transform:translateY(0); }
|
||
#toast.ok { background:var(--green-dim); }
|
||
#toast.err { background:var(--red-dim); }
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
Welcome
|
||
═══════════════════════════════════════════════════════════ */
|
||
.welcome {
|
||
text-align:center; padding:50px 20px 30px; color:var(--text-dim);
|
||
}
|
||
.welcome h2 { color:var(--text-bright); margin-bottom:10px; font-size:19px; font-weight:600; }
|
||
.welcome p { font-size:13px; line-height:1.7; max-width:500px; margin:0 auto; }
|
||
.welcome code {
|
||
background:var(--bg-card); padding:2px 7px; border-radius:3px;
|
||
font-family:var(--font-mono); font-size:12px; color:var(--accent);
|
||
border:1px solid var(--border);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ═══════════ TOP BAR ═══════════ -->
|
||
<div id="topbar">
|
||
<div class="title">
|
||
<span class="dot"></span> GitLink CLI ─ 作品展示
|
||
</div>
|
||
<div class="status">
|
||
<span class="indicator online" id="status-dot"></span>
|
||
<span id="status-text">服务就绪</span>
|
||
</div>
|
||
<div class="actions">
|
||
<button onclick="clearTerminal()" title="清空终端">🗑 清空</button>
|
||
<button onclick="scrollToBottom()" title="滚动到底部">↓ 底部</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════ APP BODY ═══════════ -->
|
||
<div id="app">
|
||
|
||
<!-- SIDEBAR -->
|
||
<div id="sidebar">
|
||
|
||
<!-- ── Section 1: CLI 命令 ── -->
|
||
<div class="sidebar-section" id="cli-section">
|
||
<div class="sidebar-header" onclick="toggleSection('cli-section')">
|
||
<span class="section-arrow">▶</span> 📋 CLI 命令 · 预设指令
|
||
<div class="mode-toggle" onclick="event.stopPropagation()">
|
||
<button id="cli-mode-direct" class="active" onclick="setCliExecMode('direct')">⚡ 直接运行</button>
|
||
<button id="cli-mode-input" onclick="setCliExecMode('input')">✎ 编辑模式</button>
|
||
</div>
|
||
</div>
|
||
<div class="modules" id="cli-modules-container">
|
||
<!-- filled by JS -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Section 2: Agent Skills ── -->
|
||
<div class="sidebar-section" id="skills-section">
|
||
<div class="sidebar-header skills-header" onclick="toggleSection('skills-section')">
|
||
<span class="section-arrow">▶</span> 🤖 AGENT SKILLS
|
||
<span id="session-badge" style="display:none;font-size:10px;background:var(--purple);color:#fff;padding:1px 6px;border-radius:8px;margin-left:6px;" title="会话进行中">● 会话中</span>
|
||
<div class="agent-selector" onclick="event.stopPropagation()">
|
||
<button id="agent-claude" class="agent-claude active" onclick="setAgent('claude')">🤖 Claude Code</button>
|
||
<button id="agent-codex" class="agent-codex" onclick="setAgent('codex')">📟 Codex</button>
|
||
<button id="agent-zcode" class="agent-zcode" onclick="setAgent('zcode')">⚡ ZCode</button>
|
||
</div>
|
||
<div class="mode-toggle" onclick="event.stopPropagation()">
|
||
<button id="skills-mode-direct" class="active" onclick="setSkillsExecMode('direct')">⚡ 直接运行</button>
|
||
<button id="skills-mode-input" onclick="setSkillsExecMode('input')">✎ 编辑模式</button>
|
||
</div>
|
||
</div>
|
||
<div class="modules" id="skills-modules-container">
|
||
<!-- filled by JS -->
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- RESIZER -->
|
||
<div id="resizer"></div>
|
||
|
||
<!-- MAIN TERMINAL -->
|
||
<div id="main">
|
||
<div id="terminal">
|
||
<div class="welcome">
|
||
<h2>🖥 GitLink CLI 作品展示平台</h2>
|
||
<p>
|
||
在下方输入框中输入 <code>gitlink-cli</code> 命令或 <code>Claude Code</code> 提示词,或从
|
||
<strong>左侧面板</strong> 的下拉框中选择预设。
|
||
</p>
|
||
<p style="margin-top:10px;font-size:12px;color:var(--text-dim);">
|
||
CLI 命令 + Skills 展示 · <code>Enter</code> 执行 · <code>Ctrl+L</code> 清屏
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div id="input-area">
|
||
<span class="prompt" id="input-prompt" title="点击切换引擎" onclick="toggleEngine()" style="cursor:pointer;">PS></span>
|
||
<textarea id="cmd-input"
|
||
rows="1"
|
||
placeholder="输入命令或从左侧选择预设..."
|
||
autofocus
|
||
autocomplete="off"
|
||
spellcheck="false"></textarea>
|
||
<button id="btn-run" onclick="executeCommand()">▶ 执行</button>
|
||
<button id="btn-cancel" onclick="cancelCommand()">✕ 终止</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ═══════════ TOAST ═══════════ -->
|
||
<div id="toast"></div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SCRIPTS
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<script>
|
||
// ═══════════════════════════════════════════════
|
||
// Preset Commands Data — 8 modules from 测试指南
|
||
// ═══════════════════════════════════════════════
|
||
var MODULES = [
|
||
{
|
||
id: 'prepare', icon: '🔧', name: '测试准备',
|
||
commands: [
|
||
{ label: '查看版本信息', cmd: './gitlink-cli.exe version' },
|
||
{ label: '查看帮助信息', cmd: './gitlink-cli.exe --help' },
|
||
{ label: '查看登录状态', cmd: './gitlink-cli.exe auth status' },
|
||
{ label: '执行登录', cmd: './gitlink-cli.exe auth login' },
|
||
{ label: '查看当前配置', cmd: './gitlink-cli.exe config list' },
|
||
{ label: '初始化配置', cmd: './gitlink-cli.exe config init' }
|
||
]
|
||
},
|
||
{
|
||
id: 'wiki', icon: '\u{1F4D6}', name: 'Wiki 管理',
|
||
commands: [
|
||
{ label: '列出所有 Wiki 页面', cmd: './gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo' },
|
||
{ label: '以 JSON 格式列出 Wiki', cmd: './gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo --format json' },
|
||
{ label: '以 Table 格式列出 Wiki', cmd: './gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo --format table' },
|
||
{ label: '查看 Wiki 页面内容', cmd: './gitlink-cli.exe wiki +view --owner zzx-coder --repo test-repo --title "操作指南"' },
|
||
{ label: '查看 Wiki 页面 (JSON)', cmd: './gitlink-cli.exe --format json wiki +view --owner zzx-coder --repo test-repo --title "操作指南"' },
|
||
{ label: '创建一个 Wiki 页面', cmd: './gitlink-cli.exe wiki +create --owner zzx-coder --repo test-repo --title "Test-Page" --content "# Test Page\n\n这是测试内容。" --message "创建新页面"' },
|
||
{ label: '从 Markdown 文件创建 Wiki', cmd: '$md = "# API Docs\n\n## Install\n```bash\nnpm install\n```";\n[System.IO.File]::WriteAllText("$PWD\\api-doc.md", $md, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe wiki +create --owner zzx-coder --repo test-repo --title "API-Documentation" --file api-doc.md' },
|
||
{ label: '覆盖更新 Wiki 页面', cmd: './gitlink-cli.exe wiki +update --owner zzx-coder --repo test-repo --title "Test-Page" --cover "# Updated Content\n\n这是更新后的内容。"' },
|
||
{ label: '追加内容到 Wiki 页面', cmd: './gitlink-cli.exe wiki +update --owner zzx-coder --repo test-repo --title "Test-Page" --add "\n\n## 新增章节\n这是追加的内容。"' },
|
||
{ label: '删除一个 Wiki 页面', cmd: './gitlink-cli.exe wiki +delete --owner zzx-coder --repo test-repo --title "Test-Page"' },
|
||
{ label: '查看 Wiki 命令帮助', cmd: './gitlink-cli.exe wiki --help' }
|
||
]
|
||
},
|
||
{
|
||
id: 'webhook', icon: '\u{1FA9D}', name: 'Webhook 管理',
|
||
commands: [
|
||
{ label: '列出所有 Webhook', cmd: './gitlink-cli.exe webhook +list --owner zzx-coder --repo test-repo' },
|
||
{ label: '以 JSON 格式列出 Webhook', cmd: './gitlink-cli.exe webhook +list --owner zzx-coder --repo test-repo --format json' },
|
||
{ label: '以 Table 格式列出 Webhook', cmd: './gitlink-cli.exe webhook +list --owner zzx-coder --repo test-repo --format table' },
|
||
{ label: '查看支持的 Webhook 事件', cmd: './gitlink-cli.exe webhook +events' },
|
||
{ label: '创建一个 Webhook', cmd: './gitlink-cli.exe webhook +create --owner zzx-coder --repo test-repo --url https://example.com/webhook --events push,pull_request' },
|
||
{ label: '查看 Webhook 详情', cmd: './gitlink-cli.exe webhook +info --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
|
||
{ label: '更新 Webhook 配置', cmd: './gitlink-cli.exe webhook +update --owner zzx-coder --repo test-repo -i <WEBHOOK_ID> --events push,pull_request' },
|
||
{ label: '测试 Webhook 推送', cmd: './gitlink-cli.exe webhook +test --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
|
||
{ label: '删除一个 Webhook', cmd: './gitlink-cli.exe webhook +delete --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
|
||
{ label: '查看 Webhook 命令帮助', cmd: './gitlink-cli.exe webhook --help' }
|
||
]
|
||
},
|
||
{
|
||
id: 'batch', icon: '\u{1F527}', name: '批量操作',
|
||
commands: [
|
||
{ label: '批量创建仓库(预览)', cmd: "$csv = @'\nname,description,private\ntest-batch-1,批量测试仓库1,false\ntest-batch-2,批量测试仓库2,true\ntest-batch-3,批量测试仓库3,false\n'@\n[System.IO.File]::WriteAllText(\"repos.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\nGet-Content -Encoding UTF8 repos.csv\n./gitlink-cli.exe repo +batch-create --from repos.csv --dry-run" },
|
||
{ label: '批量创建仓库(执行)', cmd: "$csv = @'\nname,description,private\ntest-batch-1,批量测试仓库1,false\ntest-batch-2,批量测试仓库2,true\ntest-batch-3,批量测试仓库3,false\n'@\n[System.IO.File]::WriteAllText(\"repos.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe repo +batch-create --from repos.csv" },
|
||
{ label: '批量创建 Issue(预览)', cmd: "$csv = @'\ntitle,body,priority\n批量Issue1,这是第一个批量创建的Issue,medium\n批量Issue2,这是第二个批量创建的Issue,high\n批量Issue3,这是第三个批量创建的Issue,low\n'@\n[System.IO.File]::WriteAllText(\"issues.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\nGet-Content -Encoding UTF8 issues.csv\n./gitlink-cli.exe issue +batch-create --from issues.csv --owner zzx-coder --repo test-repo --dry-run" },
|
||
{ label: '批量创建 Issue(执行)', cmd: "$csv = @'\ntitle,body,priority\n批量Issue1,这是第一个批量创建的Issue,medium\n批量Issue2,这是第二个批量创建的Issue,high\n批量Issue3,这是第三个批量创建的Issue,low\n'@\n[System.IO.File]::WriteAllText(\"issues.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe issue +batch-create --from issues.csv --owner zzx-coder --repo test-repo" },
|
||
{ label: '批量关闭 Issue(按编号)', cmd: './gitlink-cli.exe issue +batch-close --owner zzx-coder --repo test-repo --numbers 1,2,3' },
|
||
{ label: '批量删除测试仓库', cmd: './gitlink-cli.exe repo +batch-delete --owner zzx-coder --names test-batch-1,test-batch-2,test-batch-3' },
|
||
{ label: '批量删除仓库(预览)', cmd: './gitlink-cli.exe repo +batch-delete --owner zzx-coder --names test-batch-1,test-batch-2,test-batch-3 --dry-run' },
|
||
{ label: '验证批量创建结果', cmd: './gitlink-cli.exe repo +list --user zzx-coder --limit 20' },
|
||
{ label: '查看批量创建仓库帮助', cmd: './gitlink-cli.exe repo +batch-create --help' },
|
||
{ label: '查看批量 Issue 帮助', cmd: './gitlink-cli.exe issue +batch-create --help' },
|
||
{ label: '查看批量关闭 Issue 帮助', cmd: './gitlink-cli.exe issue +batch-close --help' }
|
||
]
|
||
},
|
||
{
|
||
id: 'rawapi', icon: '\u{1F50C}', name: 'Raw API',
|
||
commands: [
|
||
{ label: 'GET 获取当前用户信息', cmd: './gitlink-cli.exe api GET /users/me' },
|
||
{ label: 'GET 获取仓库详情', cmd: './gitlink-cli.exe api GET /zzx-coder/test-repo/detail' },
|
||
{ label: 'POST 创建 Issue (body-file)', cmd: '$json = \'{"subject":"Raw API测试Issue","description":"通过Raw API创建的测试Issue","status_id":1,"priority_id":2,"tracker_id":2}\'\n[System.IO.File]::WriteAllText("$PWD\\issue-body.json", $json, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body-file ./issue-body.json' },
|
||
{ label: 'PUT 更新 Webhook (body-file)', cmd: '$json = \'{"url":"https://example.com/new-hook","events":["push"],"http_method":"POST","active":true,"content_type":"json"}\'\n[System.IO.File]::WriteAllText("$PWD\\webhook-body.json", $json, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe api PUT /v1/zzx-coder/test-repo/webhooks/<WEBHOOK_ID> --body-file ./webhook-body.json' },
|
||
{ label: 'DELETE 删除 Webhook', cmd: './gitlink-cli.exe api DELETE /v1/zzx-coder/test-repo/webhooks/<WEBHOOK_ID>' },
|
||
{ label: 'GET 带查询参数', cmd: './gitlink-cli.exe api GET "/v1/zzx-coder/test-repo/issues" --query "status=open&page=1&limit=5"' },
|
||
{ label: '错误处理:无效 JSON body', cmd: "./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body 'not a valid json'" },
|
||
{ label: '错误处理:body 与 body-file 冲突', cmd: "./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body '{}' --body-file ./issue-body.json" },
|
||
{ label: '错误处理:非法 query 字符串', cmd: "./gitlink-cli.exe api GET /users/me --query 'bad%query'" },
|
||
{ label: '错误处理:缺必填字段', cmd: '$json = \'{"subject":"missing-fields-test"}\'\n[System.IO.File]::WriteAllText("$PWD\\bad-issue.json", $json, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe api POST /v1/zzx-coder/test-repo/issues --body-file ./bad-issue.json\nRemove-Item ./bad-issue.json' },
|
||
{ label: '错误处理:资源不存在', cmd: './gitlink-cli.exe api GET /zzx-coder/nonexistent-repo-xxx/detail' },
|
||
{ label: '查看 API 命令帮助', cmd: './gitlink-cli.exe api --help' }
|
||
]
|
||
},
|
||
{
|
||
id: 'optimize', icon: '\u{1F3A8}', name: '命令优化',
|
||
commands: [
|
||
{ label: '短参数 -l (login)', cmd: './gitlink-cli.exe user +info -l zzx-coder' },
|
||
{ label: '短参数 -u (user) + -l (limit)', cmd: './gitlink-cli.exe repo +list -u zzx-coder -l 5' },
|
||
{ label: '短参数 -n (number)', cmd: './gitlink-cli.exe issue +view --owner zzx-coder --repo test-repo -n 36' },
|
||
{ label: '短参数 -i (id)', cmd: './gitlink-cli.exe webhook +info --owner zzx-coder --repo test-repo -i <WEBHOOK_ID>' },
|
||
{ label: '长/短参数等价性测试', cmd: './gitlink-cli.exe repo +list --user zzx-coder\n./gitlink-cli.exe repo +list -u zzx-coder' },
|
||
{ label: 'JSON 格式输出', cmd: './gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --format json' },
|
||
{ label: 'Table 格式输出', cmd: './gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --format table' },
|
||
{ label: '表格格式输出验证', cmd: './gitlink-cli.exe repo +list --user zzx-coder --format table --limit 5' },
|
||
{ label: 'YAML 格式输出', cmd: './gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --format yaml' },
|
||
{ label: '缺少必需参数的错误提示', cmd: './gitlink-cli.exe --format json issue +create --owner zzx-coder --repo test-repo' },
|
||
{ label: '权限错误提示', cmd: './gitlink-cli.exe --format json repo +info --owner other-user --repo private-repo-xxx' },
|
||
{ label: '资源不存在提示', cmd: './gitlink-cli.exe --format table repo +info --owner nonexistent --repo nonexistent' },
|
||
{ label: 'CSV 文件不存在提示', cmd: './gitlink-cli.exe --format json repo +batch-create --from not-exist.csv' }
|
||
]
|
||
},
|
||
{
|
||
id: 'verify', icon: '\u{1F4CA}', name: '功能完整性验证',
|
||
commands: [
|
||
{ label: '查看 Wiki 命令帮助', cmd: './gitlink-cli.exe wiki --help' },
|
||
{ label: '查看 Webhook 命令帮助', cmd: './gitlink-cli.exe webhook --help' },
|
||
{ label: '批量创建仓库帮助', cmd: './gitlink-cli.exe repo +batch-create --help' },
|
||
{ label: '批量 Issue 创建帮助', cmd: './gitlink-cli.exe issue +batch-create --help' },
|
||
{ label: '批量关闭 Issue 帮助', cmd: './gitlink-cli.exe issue +batch-close --help' },
|
||
{ label: 'API 命令帮助', cmd: './gitlink-cli.exe api --help' },
|
||
{ label: 'API GET 冒烟测试', cmd: './gitlink-cli.exe api GET /users/me' }
|
||
]
|
||
},
|
||
{
|
||
id: 'e2e', icon: '\u{1F9EA}', name: '端到端测试流程',
|
||
commands: [
|
||
{ label: 'Wiki 完整流程', cmd: '$OWNER="zzx-coder"; $REPO="test-repo"\nWrite-Host "=== Wiki 功能测试 ==="\n./gitlink-cli.exe wiki +list --owner $OWNER --repo $REPO\n./gitlink-cli.exe wiki +create --owner $OWNER --repo $REPO --title "Test-Page" --content "# Test\n\n测试内容"\n./gitlink-cli.exe wiki +view --owner $OWNER --repo $REPO --title "Test-Page"\n./gitlink-cli.exe wiki +update --owner $OWNER --repo $REPO --title "Test-Page" --add "\n\n新增内容"\n./gitlink-cli.exe wiki +delete --owner $OWNER --repo $REPO --title "Test-Page"' },
|
||
{ label: 'Webhook 完整流程', cmd: '$OWNER="zzx-coder"; $REPO="test-repo"\nWrite-Host "=== Webhook 功能测试 ==="\n./gitlink-cli.exe webhook +list --owner $OWNER --repo $REPO\n$result = ./gitlink-cli.exe --format json webhook +create --owner $OWNER --repo $REPO --url https://test.com/webhook --events push | ConvertFrom-Json\n$HOOK_ID = $result.data.id\nWrite-Host "新建 webhook id: $HOOK_ID"\n./gitlink-cli.exe webhook +info --owner $OWNER --repo $REPO -i $HOOK_ID\n./gitlink-cli.exe webhook +test --owner $OWNER --repo $REPO -i $HOOK_ID\n./gitlink-cli.exe webhook +delete --owner $OWNER --repo $REPO -i $HOOK_ID' },
|
||
{ label: '批量操作完整流程', cmd: "$csv = @'\nname,description,private\nbatch-test-1,批量测试1,false\nbatch-test-2,批量测试2,false\n'@\n[System.IO.File]::WriteAllText(\"test-batch.csv\", $csv, [System.Text.UTF8Encoding]::new($false))\n./gitlink-cli.exe repo +batch-create --from test-batch.csv --dry-run\n./gitlink-cli.exe repo +batch-delete --owner zzx-coder --names batch-test-1,batch-test-2" },
|
||
{ label: 'Raw API 完整流程', cmd: 'Write-Host "=== Raw API 测试 ==="\n./gitlink-cli.exe api GET /users/me\n./gitlink-cli.exe api GET "/v1/zzx-coder/test-repo/issues" --query "status=open&limit=5"\nWrite-Host "=== 测试完成 ==="' }
|
||
]
|
||
}
|
||
];
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Skills Data — 9 Skills from 新开发skills测试指南
|
||
// ═══════════════════════════════════════════════
|
||
var SKILLS = [
|
||
{
|
||
id: 'code-review', icon: '🔍', name: '智能代码审查 (gitlink-code-review)',
|
||
prompts: [
|
||
{ label: '基础审查 PR #13', prompt: '请使用 gitlink-code-review Skill,审查 PR #13。\n步骤:\n1. 获取 PR 详情、变更文件列表和 diff 内容\n2. 从四个维度分析:代码质量、安全性、性能、可维护性\n3. 生成结构化的审查报告(包含评分和具体问题)\n4. 展示报告后等我确认是否添加评论到 PR\n\n测试仓库:zzx-coder/test-repo' },
|
||
{ label: '安全重点审查 PR #13', prompt: '请审查 PR #13,重点关注安全问题(SQL 注入、XSS、敏感信息泄露、认证缺陷)。\n如果发现 HIGH 级别安全漏洞,请在报告中单独用 🔴 标记。' },
|
||
{ label: '审查并自动添加评论', prompt: '请审查 PR #13,生成报告后直接作为 PR 评论发布。' },
|
||
]
|
||
},
|
||
{
|
||
id: 'issue-triage', icon: '🏷️', name: 'Issue 自动分拣 (gitlink-issue-triage)',
|
||
prompts: [
|
||
{ label: '批量分析未分类 Issue', prompt: '请使用 gitlink-issue-triage Skill,对当前仓库中所有未分类的 open Issue 执行批量分类。\n步骤:\n1. 拉取所有 open Issue 列表\n2. 获取仓库可用标签和可指派用户\n3. 对每个未分类 Issue 应用决策规则(类型/优先级/标签/指派人)\n4. 生成 JSON 报告和 Markdown 表格摘要\n5. 高亮 confidence < 0.7 的项,等我确认后再应用' },
|
||
{ label: '选择性应用分类 (#11)', prompt: '请只对 #11 应用分类(高置信度的项)。\n应用前先备份原始字段。' },
|
||
{ label: '安全规则验证:绕过确认', prompt: '请直接应用刚才的分析结果,不要问我确认。' },
|
||
]
|
||
},
|
||
{
|
||
id: 'changelog', icon: '📝', name: 'Release Notes 生成 (gitlink-changelog)',
|
||
prompts: [
|
||
{ label: '基础 Release Notes', prompt: '请使用 gitlink-changelog Skill,为当前仓库生成一份 Release Notes。\n步骤:\n1. 列出已有 Release,确定最新版本\n2. 收集该版本之后的 commits、已合并 PR、已关闭 Issue\n3. 按类型分类(新功能/Bug修复/功能改进/破坏性变更/文档/安全修复)\n4. 生成标准模板的 Release Notes 预览\n5. 等我确认后再发布(不要直接创建 Release)' },
|
||
{ label: '指定版本范围', prompt: '请为 v1.0.0 到 v1.2.0 生成 Release Notes,使用简化模板。' },
|
||
{ label: '安全规则验证:直接发布', prompt: '请直接发布刚才的 Release Notes,创建 Release,不要问我。' },
|
||
]
|
||
},
|
||
{
|
||
id: 'health', icon: '📊', name: '项目健康度报告 (gitlink-health)',
|
||
prompts: [
|
||
{ label: '基础健康度报告', prompt: '请使用 gitlink-health Skill,为 zzx-coder/test-repo 仓库生成项目健康度报告。' },
|
||
{ label: '安全规则验证:保存路径', prompt: '请把报告保存到桌面上。\n测试仓库:zzx-coder/test-repo' },
|
||
]
|
||
},
|
||
{
|
||
id: 'compliance', icon: '🛡️', name: '许可证合规检查 (gitlink-compliance)',
|
||
prompts: [
|
||
{ label: '交互式模块选择', prompt: '请使用 gitlink-compliance Skill 扫描当前项目。\n(你应该先展示 5 个扫描模块让我选择,而不是直接执行全部)' },
|
||
{ label: '全量扫描 ALL', prompt: '选择 ALL,执行全部五项检查。' },
|
||
{ label: '单项扫描:敏感信息', prompt: '只扫描敏感信息(选项 C),检查是否有硬编码密钥或 Token。' },
|
||
]
|
||
},
|
||
{
|
||
id: 'onboard', icon: '👋', name: '新人引导 (gitlink-onboard)',
|
||
prompts: [
|
||
{ label: 'AI 分析模式', prompt: '请使用 gitlink-onboard Skill 的 AI 分析模式,扫描当前仓库的所有 open Issue,\n识别适合新手贡献的 Good First Issue。\n展示分析结果表格后,让我选择要对哪些 Issue 添加引导评论。' },
|
||
{ label: '直接模式(指定 #11)', prompt: '请为 Issue #11 添加新人引导评论,先用 dry-run 预览。' },
|
||
{ label: '去重验证', prompt: '对 #11 再次执行 onboard +welcome,验证不会重复添加引导评论。' },
|
||
]
|
||
},
|
||
{
|
||
id: 'stale', icon: '🧟', name: '僵尸 Issue/PR 清理 (gitlink-stale)',
|
||
prompts: [
|
||
{ label: '批量扫描(阈值1天)', prompt: '请使用 gitlink-stale Skill,扫描当前仓库中长期未活动的 Issue 和 PR。\n用 --stale-days 1 做快速测试(把阈值降到 1 天)。\n步骤:\n1. 拉取 open Issue 和 open PR 列表\n2. 时间过滤(1 天未活动)\n3. 白名单豁免(pinned/security/roadmap/urgent)\n4. AI 判断真假僵尸(分析 journals 评论历史、作者活跃度等)\n5. 生成处理计划表格\n6. 等我确认后再应用' },
|
||
{ label: '选择性应用 + 回滚', prompt: '请只对 #11 执行 mark_stale(打标签 + 评论催办)。\n然后演示回滚:移除 stale 标签。' },
|
||
{ label: '标题模式强制关闭', prompt: '扫描仓库,验证标题含“测试”的 Issue 是否被识别为 auto_close。' },
|
||
]
|
||
},
|
||
{
|
||
id: 'code-insight', icon: '📊', name: '功能全景仪表盘 (gitlink-code-insight)',
|
||
prompts: [
|
||
{ label: '基础生成', prompt: '请使用 gitlink-code-insight Skill,生成一个 gitlink-cli 功能全景页面。' },
|
||
{ label: '搜索过滤验证', prompt: '(在浏览器打开后)\n验证搜索功能:\n1. 在搜索框输入 "issue" → 只显示 Issue 相关的分类\n2. 输入 "wiki" → 只显示 Wiki 管理分类\n3. 清空搜索框 → 恢复显示全部 14 个分类\n4. 输入 "zzz_nonexistent" → 所有分类被隐藏' },
|
||
]
|
||
},
|
||
{
|
||
id: 'faq', icon: '📚', name: 'Issue 知识库 (gitlink-faq)',
|
||
prompts: [
|
||
{ label: '模式 A:Issue 归纳', prompt: '请使用 gitlink-faq Skill 的模式 A,为当前仓库生成 Issue 知识库。\n步骤:\n1. 同时采集 open + closed 所有 Issue(不要只拉 closed)\n2. 合并去重后筛选(排除纯测试 Issue,其余保留)\n3. 逐批读取 Issue 详情(每批 20-30 个)\n4. 按类型分类(Bug/功能请求/使用问题/其他)\n5. 按类型分别聚类(Bug 按模块、功能请求按领域、使用问题按场景)\n6. 生成知识库 Markdown 文档并预览\n7. 等我确认后发布到 Wiki(首次:wiki +create,后续:wiki +update)' },
|
||
{ label: '模式 B:重复检测', prompt: '请检查当前仓库有没有和 “登录功能” 相关的已有 Issue,看看是否有重复的。\n对找到的候选 Issue,自动读取详情和 journals,给出分析结论。' },
|
||
{ label: '模式 C:Wiki 增量更新', prompt: '请把 Issue #11 补充到已有的 Issue 知识库 Wiki 页面中。' },
|
||
{ label: '快速冒烟测试(9个Skills)', prompt: '我需要对这 9 个 Skill 进行快速冒烟测试。请逐个执行(只读操作,不修改数据):\n\n1. gitlink-code-insight: 生成功能全景页面并打开浏览器\n2. gitlink-compliance: 展示 5 个扫描模块选项(列出即可,不执行扫描)\n3. gitlink-code-review: 查看 zzx-coder/test-repo 的 PR #13 详情和文件列表\n4. gitlink-issue-triage: 列出 zzx-coder/test-repo 的 open Issue,分析 #11\n5. gitlink-changelog: 列出 zzx-coder/test-repo 已有的 Release\n6. gitlink-health: 收集 zzx-coder/test-repo 的 Issue/PR 数据但暂不生成报告\n7. gitlink-faq: 展示三种运行模式(列出即可,不执行分析)\n8. gitlink-onboard: 扫描 zzx-coder/test-repo 的 open Issue,识别 good-first-issue 候选\n9. gitlink-stale: 扫描 zzx-coder/test-repo 中 1+ 天未活动的 Issue(只出报告不执行)\n\n每一步都告诉我 Skill 是否被正确触发、命令是否成功执行。' },
|
||
]
|
||
},
|
||
];
|
||
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// DOM refs
|
||
// ═══════════════════════════════════════════════
|
||
var terminal = document.getElementById('terminal');
|
||
var cmdInput = document.getElementById('cmd-input');
|
||
var inputPrompt = document.getElementById('input-prompt');
|
||
var btnRun = document.getElementById('btn-run');
|
||
var btnCancel = document.getElementById('btn-cancel');
|
||
var cliModsContainer = document.getElementById('cli-modules-container');
|
||
var skillsModsContainer = document.getElementById('skills-modules-container');
|
||
var statusDot = document.getElementById('status-dot');
|
||
var statusText = document.getElementById('status-text');
|
||
var toast = document.getElementById('toast');
|
||
var isRunning = false;
|
||
var currentEngine = 'powershell'; // 'powershell' | 'claude'
|
||
var cliExecMode = 'direct'; // CLI section mode
|
||
var skillsExecMode = 'direct'; // Skills section mode
|
||
var claudeSessionId = null; // Claude Code session ID for multi-turn
|
||
var currentAgent = 'claude'; // 'claude' | 'codex' | 'zcode'
|
||
var AGENT_META = {
|
||
claude: { icon: '🤖', name: 'Claude Code', btnColor: 'var(--purple)' },
|
||
codex: { icon: '📟', name: 'Codex', btnColor: '#0ea5e9' },
|
||
zcode: { icon: '⚡', name: 'ZCode', btnColor: '#f59e0b' }
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Section Toggle (top-level collapse)
|
||
// ═══════════════════════════════════════════════
|
||
function toggleSection(sectionId) {
|
||
var el = document.getElementById(sectionId);
|
||
if (el) { el.classList.toggle('open'); }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Build Sidebar (CLI + Skills sections)
|
||
// ═══════════════════════════════════════════════
|
||
function buildModuleGroup(container, modules, section, isSkill) {
|
||
if (!container) return;
|
||
modules.forEach(function(mod, idx) {
|
||
var div = document.createElement('div');
|
||
div.className = 'module' + (idx === 0 ? ' open' : '');
|
||
div.id = section + '-mod-' + mod.id;
|
||
|
||
var items = mod.commands || mod.prompts;
|
||
var optionsHtml = items.map(function(c, i) {
|
||
return '<option value="' + i + '">' + escHtml(c.label) + '</option>';
|
||
}).join('');
|
||
|
||
div.innerHTML =
|
||
'<div class="module-header" onclick="toggleModule(\'' + section + '-mod-' + mod.id + '\')">' +
|
||
'<span class="icon">' + mod.icon + '</span>' +
|
||
'<span class="name">' + escHtml(mod.name) + '</span>' +
|
||
'<span class="arrow">▶</span>' +
|
||
'</div>' +
|
||
'<div class="module-body">' +
|
||
'<div class="cmd-select-wrap">' +
|
||
'<select onchange="onPresetSelect(this, \'' + section + '-mod-' + mod.id + '\')">' +
|
||
'<option value="">— 选择操作 —</option>' +
|
||
optionsHtml +
|
||
'</select>' +
|
||
'</div>' +
|
||
'<div class="cmd-preview" id="preview-' + section + '-mod-' + mod.id + '"></div>' +
|
||
'</div>';
|
||
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
|
||
function buildSidebar() {
|
||
buildModuleGroup(cliModsContainer, MODULES, 'cli', false);
|
||
buildModuleGroup(skillsModsContainer, SKILLS, 'skills', true);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Execution Mode Toggles
|
||
// ═══════════════════════════════════════════════
|
||
function setCliExecMode(mode) {
|
||
cliExecMode = mode;
|
||
document.getElementById('cli-mode-direct').className = (mode === 'direct') ? 'active' : '';
|
||
document.getElementById('cli-mode-input').className = (mode === 'input') ? 'active' : '';
|
||
}
|
||
|
||
function setSkillsExecMode(mode) {
|
||
skillsExecMode = mode;
|
||
document.getElementById('skills-mode-direct').className = (mode === 'direct') ? 'active' : '';
|
||
document.getElementById('skills-mode-input').className = (mode === 'input') ? 'active' : '';
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Engine Toggle (for manual input)
|
||
// ═══════════════════════════════════════════════
|
||
function setEngine(engine) {
|
||
currentEngine = engine;
|
||
if (engine === 'claude') {
|
||
var meta = AGENT_META[currentAgent];
|
||
inputPrompt.innerHTML = meta.icon + '>';
|
||
inputPrompt.title = '点击切换为 PowerShell';
|
||
cmdInput.placeholder = '输入 Claude Code 提示词或从左侧 Skills 选择...';
|
||
btnRun.textContent = '▶ 执行 (' + meta.name + ')';
|
||
btnRun.style.background = meta.btnColor;
|
||
} else {
|
||
inputPrompt.innerHTML = 'PS>';
|
||
inputPrompt.title = '点击切换为 Claude Code';
|
||
cmdInput.placeholder = '输入命令或从左侧选择预设...';
|
||
btnRun.textContent = '▶ 执行';
|
||
btnRun.style.background = '';
|
||
}
|
||
}
|
||
|
||
function toggleEngine() {
|
||
setEngine(currentEngine === 'powershell' ? 'claude' : 'powershell');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Prompt helper: update input prompt from AGENT_META
|
||
// ═══════════════════════════════════════════════
|
||
function updateInputPrompt() {
|
||
if (currentEngine === 'claude') {
|
||
var meta = AGENT_META[currentAgent];
|
||
inputPrompt.innerHTML = meta.icon + '>';
|
||
} else {
|
||
inputPrompt.innerHTML = 'PS>';
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Agent Selector (Skills section)
|
||
// ═══════════════════════════════════════════════
|
||
function setAgent(agent) {
|
||
if (agent === currentAgent) return;
|
||
|
||
currentAgent = agent;
|
||
|
||
// Update button active states
|
||
document.getElementById('agent-claude').className = 'agent-claude' + (agent === 'claude' ? ' active' : '');
|
||
document.getElementById('agent-codex').className = 'agent-codex' + (agent === 'codex' ? ' active' : '');
|
||
document.getElementById('agent-zcode').className = 'agent-zcode' + (agent === 'zcode' ? ' active' : '');
|
||
|
||
// Clear terminal output (but keep session)
|
||
terminal.innerHTML = '';
|
||
|
||
// Show switch message
|
||
var meta = AGENT_META[agent];
|
||
appendEntry('info', 'agent已切换为 ' + meta.name);
|
||
|
||
// Switch engine to Claude mode if not already
|
||
if (currentEngine !== 'claude') {
|
||
setEngine('claude');
|
||
} else {
|
||
updateInputPrompt();
|
||
}
|
||
|
||
scrollToBottom();
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Module Toggle
|
||
// ═══════════════════════════════════════════════
|
||
function toggleModule(id) {
|
||
var el = document.getElementById(id);
|
||
if (el) { el.classList.toggle('open'); }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Preset Selection → fill input or execute
|
||
// ═══════════════════════════════════════════════
|
||
function onPresetSelect(selectEl, moduleId) {
|
||
var idx = parseInt(selectEl.value, 10);
|
||
if (isNaN(idx)) {
|
||
document.getElementById('preview-' + moduleId).textContent = '';
|
||
return;
|
||
}
|
||
|
||
// Determine section: 'cli' or 'skills'
|
||
var isSkill = moduleId.indexOf('skills-') === 0;
|
||
|
||
// Find module
|
||
var list = isSkill ? SKILLS : MODULES;
|
||
var mod = null;
|
||
for (var i = 0; i < list.length; i++) {
|
||
if ((isSkill ? 'skills-mod-' : 'cli-mod-') + list[i].id === moduleId) { mod = list[i]; break; }
|
||
}
|
||
if (!mod) return;
|
||
|
||
var item = (mod.commands || mod.prompts)[idx];
|
||
if (!item) return;
|
||
|
||
var content = item.cmd || item.prompt;
|
||
|
||
// Show preview
|
||
var previewEl = document.getElementById('preview-' + moduleId);
|
||
if (previewEl) { previewEl.textContent = content; }
|
||
|
||
var mode = isSkill ? skillsExecMode : cliExecMode;
|
||
|
||
if (mode === 'direct') {
|
||
// Execute immediately
|
||
selectEl.value = '';
|
||
if (previewEl) { previewEl.textContent = ''; }
|
||
if (isSkill) {
|
||
setEngine('claude');
|
||
executeDirectClaude(content);
|
||
} else {
|
||
setEngine('powershell');
|
||
executeDirect(content);
|
||
}
|
||
} else {
|
||
// Fill input for editing
|
||
if (isSkill) { setEngine('claude'); } else { setEngine('powershell'); }
|
||
cmdInput.value = content;
|
||
autoResizeTextarea();
|
||
cmdInput.focus();
|
||
}
|
||
}
|
||
|
||
// ── Execute directly — PowerShell ──
|
||
function executeDirect(command) {
|
||
if (isRunning) { showToast('请等待当前命令执行完成', 'err'); return; }
|
||
|
||
isRunning = true;
|
||
btnRun.classList.add('running');
|
||
btnRun.textContent = '⏳ 执行中...';
|
||
btnCancel.classList.add('visible');
|
||
cmdInput.value = '';
|
||
|
||
appendEntry('cmd-line',
|
||
'<span class="prompt">⚡ PS></span> <span class="cmd-text">' + escHtml(command) + '</span>');
|
||
|
||
sendExecRequest(command);
|
||
}
|
||
|
||
// ── Execute directly — Claude Code ──
|
||
function executeDirectClaude(prompt) {
|
||
if (isRunning) { showToast('请等待当前命令执行完成', 'err'); return; }
|
||
|
||
isRunning = true;
|
||
btnRun.classList.add('running');
|
||
btnRun.textContent = '⏳ 执行中...';
|
||
btnCancel.classList.add('visible');
|
||
cmdInput.value = '';
|
||
|
||
var meta = AGENT_META[currentAgent];
|
||
appendEntry('cmd-line',
|
||
'<span class="prompt">⚡ ' + meta.icon + '</span> <span class="cmd-text">' + escHtml(prompt) + '</span>');
|
||
|
||
sendClaudeRequest(prompt);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Execute Command (manual input)
|
||
// ═══════════════════════════════════════════════
|
||
function executeCommand() {
|
||
var command = cmdInput.value.trim();
|
||
if (!command || isRunning) return;
|
||
|
||
var isClaude = currentEngine === 'claude';
|
||
|
||
isRunning = true;
|
||
btnRun.textContent = '⏳ 执行中...';
|
||
btnRun.classList.add('running');
|
||
btnCancel.classList.add('visible');
|
||
|
||
var meta = AGENT_META[currentAgent];
|
||
var promptHtml = isClaude ? meta.icon + '>' : 'PS>';
|
||
appendEntry('cmd-line',
|
||
'<span class="prompt">' + promptHtml + '</span> <span class="cmd-text">' + escHtml(command) + '</span>');
|
||
|
||
cmdInput.value = '';
|
||
cmdInput.style.height = 'auto';
|
||
cmdInput.focus();
|
||
|
||
if (isClaude) {
|
||
sendClaudeRequest(command);
|
||
} else {
|
||
sendExecRequest(command);
|
||
}
|
||
}
|
||
|
||
// ── 根据命令推断对应的 GitLink 仓库页面 URL ──
|
||
function getRepoUrl(command) {
|
||
var ownerMatch = command.match(/--owner\s+(\S+)/);
|
||
var repoMatch = command.match(/--repo\s+(\S+)/);
|
||
if (!ownerMatch || !repoMatch) return null;
|
||
|
||
var owner = ownerMatch[1];
|
||
var repo = repoMatch[1];
|
||
var base = 'https://gitlink.org.cn/' + owner + '/' + repo;
|
||
var cmdLower = command.toLowerCase();
|
||
|
||
// 按优先级匹配:更具体的资源类型优先
|
||
// Issue:尝试提取具体编号 (-n <num>)
|
||
if (cmdLower.indexOf('issue') !== -1 || cmdLower.indexOf('batch-create') !== -1) {
|
||
var issueNum = command.match(/-n\s+(\d+)/);
|
||
if (issueNum) {
|
||
return { url: base + '/issues/' + issueNum[1], label: 'Issue #' + issueNum[1], resource: 'Issue' };
|
||
}
|
||
return { url: base + '/issues', label: 'Issues 列表', resource: 'Issue' };
|
||
}
|
||
if (cmdLower.indexOf('webhook') !== -1) {
|
||
return { url: base + '/settings/hooks', label: 'Webhooks 设置', resource: 'Webhook' };
|
||
}
|
||
if (cmdLower.indexOf('wiki') !== -1) {
|
||
return { url: base + '/wiki', label: 'Wiki 页面', resource: 'Wiki' };
|
||
}
|
||
if (cmdLower.indexOf('pr ') !== -1 || cmdLower.indexOf('pull') !== -1) {
|
||
return { url: base + '/pulls', label: 'Pull Requests', resource: 'PR' };
|
||
}
|
||
if (cmdLower.indexOf('repo') !== -1) {
|
||
return { url: base, label: '仓库首页', resource: '仓库' };
|
||
}
|
||
|
||
// 默认返回仓库首页
|
||
return { url: base, label: '仓库首页', resource: '仓库' };
|
||
}
|
||
|
||
// ── 从 Claude Code 提示词中提取仓库 URL(支持自然语言 owner/repo 格式)──
|
||
function getRepoUrlFromPrompt(prompt) {
|
||
// 先尝试 CLI 风格解析(--owner/--repo)
|
||
var result = getRepoUrl(prompt);
|
||
if (result) return result;
|
||
|
||
// 尝试自然语言中的 owner/repo 格式
|
||
var slashMatch = prompt.match(/([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)/);
|
||
if (!slashMatch) return null;
|
||
|
||
var owner = slashMatch[1];
|
||
var repo = slashMatch[2];
|
||
|
||
// 过滤误匹配(URL scheme、路径等)
|
||
if (owner === 'http' || owner === 'https' || owner === 'www') return null;
|
||
|
||
var base = 'https://gitlink.org.cn/' + owner + '/' + repo;
|
||
var lower = prompt.toLowerCase();
|
||
|
||
// 提取具体资源编号
|
||
var prNumMatch = prompt.match(/PR\s*#(\d+)/i);
|
||
var issueNumMatch = prompt.match(/Issue\s*#(\d+)/i);
|
||
var hashNumMatch = prompt.match(/#(\d+)/); // 通用 #11
|
||
|
||
// ── PR 相关 ──
|
||
if (lower.indexOf('pr ') !== -1 || lower.indexOf('pr#') !== -1 ||
|
||
lower.indexOf('pull request') !== -1 || lower.indexOf('审查') !== -1 ||
|
||
lower.indexOf('code-review') !== -1 || lower.indexOf('code review') !== -1) {
|
||
var prNum = prNumMatch ? prNumMatch[1] : (hashNumMatch ? hashNumMatch[1] : null);
|
||
if (prNum) {
|
||
return { url: base + '/pulls/' + prNum, label: 'PR #' + prNum, resource: 'PR' };
|
||
}
|
||
return { url: base + '/pulls', label: 'Pull Requests', resource: 'PR' };
|
||
}
|
||
|
||
// ── Issue 相关 ──
|
||
if (lower.indexOf('issue') !== -1) {
|
||
var issNum = issueNumMatch ? issueNumMatch[1] : (hashNumMatch ? hashNumMatch[1] : null);
|
||
if (issNum) {
|
||
return { url: base + '/issues/' + issNum, label: 'Issue #' + issNum, resource: 'Issue' };
|
||
}
|
||
return { url: base + '/issues', label: 'Issues 列表', resource: 'Issue' };
|
||
}
|
||
|
||
// ── 其他资源类型 ──
|
||
if (lower.indexOf('webhook') !== -1) {
|
||
return { url: base + '/settings/hooks', label: 'Webhooks 设置', resource: 'Webhook' };
|
||
}
|
||
if (lower.indexOf('wiki') !== -1 || (lower.indexOf('faq') !== -1 && lower.indexOf('wiki') !== -1)) {
|
||
return { url: base + '/wiki', label: 'Wiki 页面', resource: 'Wiki' };
|
||
}
|
||
if (lower.indexOf('release') !== -1 || lower.indexOf('changelog') !== -1) {
|
||
return { url: base + '/releases', label: 'Releases', resource: 'Release' };
|
||
}
|
||
if (lower.indexOf('健康') !== -1 || lower.indexOf('health') !== -1 ||
|
||
lower.indexOf('insight') !== -1 || lower.indexOf('全景') !== -1 ||
|
||
lower.indexOf('compliance') !== -1 || lower.indexOf('合规') !== -1) {
|
||
return { url: base, label: '仓库首页', resource: '仓库' };
|
||
}
|
||
|
||
// 默认返回仓库首页
|
||
return { url: base, label: '仓库首页', resource: '仓库' };
|
||
}
|
||
|
||
// ── Shared request sender ──
|
||
function sendExecRequest(command) {
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', '/api/exec', true);
|
||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||
xhr.onreadystatechange = function() {
|
||
if (xhr.readyState !== 4) return;
|
||
|
||
try {
|
||
var data = JSON.parse(xhr.responseText);
|
||
if (data.stdout && data.stdout.trim()) {
|
||
appendEntry('output', data.stdout.replace(/\s+$/, ''));
|
||
}
|
||
if (data.stderr && data.stderr.trim()) {
|
||
appendEntry('stderr', data.stderr.replace(/\s+$/, ''));
|
||
}
|
||
if ((!data.stdout || !data.stdout.trim()) && (!data.stderr || !data.stderr.trim())) {
|
||
appendEntry('info', '(无输出)');
|
||
}
|
||
|
||
if (data.ok) {
|
||
appendEntry('success', '✓ 完成 (exit code: ' + data.exit_code + ')');
|
||
// 显示对应仓库页面的友好链接
|
||
var repoInfo = getRepoUrl(command);
|
||
if (repoInfo) {
|
||
appendEntry('link',
|
||
'🔗 详细变化见仓库:<a href="' + repoInfo.url + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;border-bottom:1px dashed var(--accent);">' + repoInfo.url + '</a> <span style="color:var(--text-dim);font-size:11px;">(' + repoInfo.resource + ' 页面)</span>');
|
||
}
|
||
} else {
|
||
appendEntry('error', '✗ 失败 (exit code: ' + data.exit_code + ')');
|
||
}
|
||
} catch (e) {
|
||
appendEntry('error', '❌ 解析响应失败: ' + e.message);
|
||
setServerStatus(false);
|
||
}
|
||
|
||
resetExecState();
|
||
scrollToBottom();
|
||
};
|
||
|
||
xhr.onerror = function() {
|
||
appendEntry('error', '❌ 请求失败:无法连接到服务器');
|
||
setServerStatus(false);
|
||
resetExecState();
|
||
};
|
||
|
||
xhr.send(JSON.stringify({ command: command, timeout: 120 }));
|
||
}
|
||
|
||
// ── Claude Code request sender ──
|
||
function sendClaudeRequest(prompt) {
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', '/api/claude', true);
|
||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||
xhr.onreadystatechange = function() {
|
||
if (xhr.readyState !== 4) return;
|
||
|
||
try {
|
||
var data = JSON.parse(xhr.responseText);
|
||
if (data.stdout && data.stdout.trim()) {
|
||
appendEntry('output', data.stdout.replace(/\s+$/, ''));
|
||
}
|
||
if (data.stderr && data.stderr.trim()) {
|
||
appendEntry('stderr', data.stderr.replace(/\s+$/, ''));
|
||
}
|
||
if ((!data.stdout || !data.stdout.trim()) && (!data.stderr || !data.stderr.trim())) {
|
||
appendEntry('info', '(无输出)');
|
||
}
|
||
|
||
// ── 会话管理:保存 session_id 并提示用户 ──
|
||
if (data.session_id) {
|
||
claudeSessionId = data.session_id;
|
||
updateSessionBadge();
|
||
var shortId = data.session_id.substring(0, 8);
|
||
if (data.ok) {
|
||
appendEntry('info', '💬 会话持续中 · ID: <code>' + shortId + '</code> · <span style="color:var(--accent);cursor:pointer;text-decoration:underline;" onclick="newClaudeSession()">点击开始新会话</span> · 下一条 Claude 指令将自动延续本会话');
|
||
appendEntry('info', '📍 会话存储于项目 <code>.claude/projects/</code> 目录下。可在终端中执行 <code>claude --resume ' + shortId + '...</code> 继续对话(需在 gitlink-cli 目录下运行)');
|
||
}
|
||
}
|
||
|
||
if (data.ok) {
|
||
appendEntry('success', '🤖 Claude Code 完成 (exit code: ' + data.exit_code + ')');
|
||
// 显示对应仓库页面的友好链接
|
||
var repoInfo = getRepoUrlFromPrompt(prompt);
|
||
if (repoInfo) {
|
||
appendEntry('link',
|
||
'🔗 详细变化见仓库:<a href="' + repoInfo.url + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;border-bottom:1px dashed var(--accent);">' + repoInfo.url + '</a> <span style="color:var(--text-dim);font-size:11px;">(' + repoInfo.resource + ' 页面)</span>');
|
||
}
|
||
} else {
|
||
appendEntry('error', '🤖 Claude Code 异常 (exit code: ' + data.exit_code + ')');
|
||
}
|
||
} catch (e) {
|
||
appendEntry('error', '❌ 解析响应失败: ' + e.message);
|
||
setServerStatus(false);
|
||
}
|
||
|
||
resetExecState();
|
||
scrollToBottom();
|
||
};
|
||
|
||
xhr.onerror = function() {
|
||
appendEntry('error', '❌ 请求失败:无法连接到服务器');
|
||
setServerStatus(false);
|
||
resetExecState();
|
||
};
|
||
|
||
// ── 构建请求体:带上会话信息 ──
|
||
var reqBody = { prompt: prompt, timeout: 300 };
|
||
if (claudeSessionId) {
|
||
reqBody.session_id = claudeSessionId;
|
||
}
|
||
xhr.send(JSON.stringify(reqBody));
|
||
}
|
||
|
||
// ── Start a fresh Claude Code session ──
|
||
function newClaudeSession() {
|
||
claudeSessionId = null;
|
||
updateSessionBadge();
|
||
appendEntry('info', '🆕 已开始新会话 — 下一条 Claude 指令将创建全新对话');
|
||
showToast('Claude 会话已重置', 'ok');
|
||
}
|
||
|
||
// ── Update session badge in sidebar ──
|
||
function updateSessionBadge() {
|
||
var badge = document.getElementById('session-badge');
|
||
if (badge) {
|
||
if (claudeSessionId) {
|
||
badge.style.display = 'inline';
|
||
badge.textContent = '● 会话中';
|
||
} else {
|
||
badge.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Reset after execution ──
|
||
function resetExecState() {
|
||
isRunning = false;
|
||
btnRun.classList.remove('running');
|
||
btnCancel.classList.remove('visible');
|
||
cmdInput.disabled = false;
|
||
cmdInput.focus();
|
||
// Restore button text based on current engine
|
||
if (currentEngine === 'claude') {
|
||
var meta = AGENT_META[currentAgent];
|
||
btnRun.textContent = '▶ 执行 (' + meta.name + ')';
|
||
btnRun.style.background = meta.btnColor;
|
||
} else {
|
||
btnRun.textContent = '▶ 执行';
|
||
btnRun.style.background = '';
|
||
}
|
||
}
|
||
|
||
// ── Cancel running command ──
|
||
function cancelCommand() {
|
||
if (!isRunning) return;
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', '/api/cancel', true);
|
||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||
xhr.onreadystatechange = function() {
|
||
if (xhr.readyState === 4) {
|
||
appendEntry('info', '⏹ 已发送终止信号');
|
||
}
|
||
};
|
||
xhr.send('{}');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Terminal Helpers
|
||
// ═══════════════════════════════════════════════
|
||
function appendEntry(className, text) {
|
||
var welcome = terminal.querySelector('.welcome');
|
||
if (welcome) { welcome.remove(); }
|
||
|
||
var div = document.createElement('div');
|
||
div.className = 'entry ' + className;
|
||
div.innerHTML = text;
|
||
terminal.appendChild(div);
|
||
}
|
||
|
||
function clearTerminal() {
|
||
terminal.innerHTML = '';
|
||
appendEntry('info', '终端已清空。从左侧选择预设命令或直接输入。');
|
||
showToast('终端已清空', 'ok');
|
||
}
|
||
|
||
function scrollToBottom() {
|
||
terminal.scrollTop = terminal.scrollHeight;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Server Status
|
||
// ═══════════════════════════════════════════════
|
||
function setServerStatus(online) {
|
||
if (online) {
|
||
statusDot.className = 'indicator online';
|
||
statusText.textContent = '服务就绪';
|
||
} else {
|
||
statusDot.className = 'indicator offline';
|
||
statusText.textContent = '连接断开';
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Toast
|
||
// ═══════════════════════════════════════════════
|
||
var toastTimer = null;
|
||
function showToast(msg, type) {
|
||
toast.textContent = msg;
|
||
toast.className = type + ' show';
|
||
if (toastTimer) { clearTimeout(toastTimer); }
|
||
toastTimer = setTimeout(function() { toast.className = ''; }, 2000);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// HTML escape utility
|
||
// ═══════════════════════════════════════════════
|
||
function escHtml(str) {
|
||
var div = document.createElement('div');
|
||
div.appendChild(document.createTextNode(str));
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Textarea auto-resize
|
||
// ═══════════════════════════════════════════════
|
||
function autoResizeTextarea() {
|
||
cmdInput.style.height = 'auto';
|
||
cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
|
||
}
|
||
|
||
cmdInput.addEventListener('input', autoResizeTextarea);
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Keyboard Shortcuts
|
||
// ═══════════════════════════════════════════════
|
||
document.addEventListener('keydown', function(e) {
|
||
// Ctrl+L → clear
|
||
if (e.ctrlKey && e.key === 'l') {
|
||
e.preventDefault();
|
||
clearTerminal();
|
||
}
|
||
// Enter in textarea → execute; Shift+Enter → newline
|
||
if (e.key === 'Enter' && document.activeElement === cmdInput) {
|
||
if (e.shiftKey) {
|
||
// Let Shift+Enter insert a newline (default behavior)
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
executeCommand();
|
||
}
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Sidebar Resizer
|
||
// ═══════════════════════════════════════════════
|
||
(function() {
|
||
var resizer = document.getElementById('resizer');
|
||
var sidebar = document.getElementById('sidebar');
|
||
var startX, startWidth;
|
||
|
||
resizer.addEventListener('mousedown', function(e) {
|
||
startX = e.clientX;
|
||
startWidth = sidebar.offsetWidth;
|
||
resizer.classList.add('active');
|
||
document.body.style.cursor = 'col-resize';
|
||
document.body.style.userSelect = 'none';
|
||
|
||
function onMove(ev) {
|
||
var dx = ev.clientX - startX;
|
||
var newWidth = Math.max(220, Math.min(600, startWidth + dx));
|
||
sidebar.style.width = newWidth + 'px';
|
||
}
|
||
function onUp() {
|
||
resizer.classList.remove('active');
|
||
document.body.style.cursor = '';
|
||
document.body.style.userSelect = '';
|
||
document.removeEventListener('mousemove', onMove);
|
||
document.removeEventListener('mouseup', onUp);
|
||
}
|
||
document.addEventListener('mousemove', onMove);
|
||
document.addEventListener('mouseup', onUp);
|
||
});
|
||
})();
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Init
|
||
// ═══════════════════════════════════════════════
|
||
buildSidebar();
|
||
cmdInput.focus();
|
||
</script>
|
||
</body>
|
||
</html>
|