Files
CloudSSH/docs/theme-editor/index.html
T
TanXinandTanXin 918bd69fbb feat: 实现 AI Agent 运维助手完整功能,优化前端交互与并发稳定性,健全安全策略与异常保活机制 (#23)
## 摘要 (Summary)
本 PR 整合了 `test` 分支中关于 **AI Agent 运维助手系统**
的核心实现、后续优化以及一系列高并发、竞态条件、连接保活及接口报错的修复工作。项目现已具备功能完整、安全可控、交互流畅且高度稳定的 AI
助手侧边栏。

---

## 核心变更内容 (Key Changes)

### 1. AI Agent 核心功能与工具链扩展
* **工具箱扩展**:实现了 8 个运维相关的核心工具,包括
`detect_environment`(环境探测)、`list_processes`(进程列表)、`service_manage`(服务管理)、`docker_manage`(容器管理)以及基础终端执行工具。
* **流式输出与 Markdown**:AI 助手的回复支持逐字流式加载显示,完成后使用 `marked` + `DOMPurify`
渲染(支持 GFM 表格),替代了原有的手写解析器。
*
**上下文管理**:引入了对话历史摘要机制(`trimMessages`),优化了上下文管理策略,同时将终端及环境上下文移入系统提示词(System
Prompt),防止跨请求历史数据重复与丢失。

### 2. 前端交互与 UX 深度优化
* **折叠式思考过程**:实现了 AI
思考过程折叠容器,工具调用不再占用主回复空间;支持思考步骤的实时预览与交互,完成后展开可查看完整执行链路。
* **样式与主题调整**:优化了 Agent 侧边栏宽度、输入框 Placeholder 颜色、发送按钮样式、自动换行与主题颜色的同步预览。
*   **输入流并发保护**:在安全弹窗确认期间显式禁用输入,以“完成/取消”代替粗暴中断,避免指令状态冲突。

### 3. 并发、安全与系统稳定性修复 (核心痛点)
* **安全双层拦截**:重构安全分级策略,采用“LLM 主观判断 + 工具层硬编码规则兜底拦截”的双层过滤。对高危操作(如包管理
`apt/yum` 等)以及 `sudo` 操作增加强制用户确认流程。
* **彻底消除 API 400 错误**:修复了在消息裁剪、指令 Abort、用户取消等场景下,因 `tool_calls`
孤儿消息或消息不配对导致的大模型接口报 400 错误的问题;增加了消息完整性校验,自动过滤残留/不配对的工具消息。
*   **防止 DO 冬眠与卡死**:
* 在 `runLoop` 中引入 5 秒一次的 `keepAlive` 保活机制,防止 Cloudflare Durable Object
在等待大模型响应或等待用户确认期间因无操作而冬眠。
    *   将保活定时器提升为类字段,防止关闭连接时发生定时器泄漏。
*   **网络与并发修复**:
    *   修复了多命令执行导致的 SSH 频繁断开和前端 WebSocket 重连风暴。
* 解决了 `Channel removed before open` 竞态条件,以及 exec channel 未释放导致 SSH 报错
`Channel open failed (reason=2)` 的 channel 泄漏问题。
    *   为 WebSocket Handler 增加了全局错误边界,避免未捕获异常导致 DO 意外崩溃断开。

### 4. SSH 基础体验优化
* **主机密钥验证优化 (TOFU)**:重构了主机密钥指纹变更提示,拆分多行展示已知指纹与实际指纹的对比,并动态识别并展示密钥算法类型(如
`ssh-ed25519`)。

### 5. 文档与部署优化
*   **一键部署**:为 README 添加了 Cloudflare 一键部署按钮,并更新了最新的演示视频。
*   **开发规约**:更新了 `AGENTS.md`,同步了 AI Agent 相关的 8 大工具、保活看门狗及两层安全策略的开发维护说明。

---------

Co-authored-by: TanXin <tanxin@example.com>
2026-07-08 21:43:38 +08:00

1972 lines
60 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>CloudSSH Theme Editor</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'JetBrains Mono', monospace;
background: #0f0f0f;
color: #ccc;
height: 100vh;
overflow: hidden;
}
.editor-layout {
display: flex;
height: 100vh;
}
/* ===== Sidebar ===== */
.sidebar {
width: 340px;
min-width: 340px;
background: #161616;
border-right: 1px solid #2a2a2a;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid #2a2a2a;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.sidebar-header h1 {
font-size: 14px;
font-weight: 700;
color: #4af626;
letter-spacing: 0.05em;
}
.sidebar-actions {
display: flex;
gap: 6px;
}
.sidebar-actions button {
background: transparent;
border: 1px solid #333;
color: #aaa;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
padding: 4px 8px;
cursor: pointer;
transition: all 0.15s;
}
.sidebar-actions button:hover {
border-color: #4af626;
color: #4af626;
}
.sidebar-actions button.primary {
border-color: #4af626;
color: #4af626;
}
.sidebar-actions button.primary:hover {
background: #4af626;
color: #000;
}
.presets-bar {
padding: 12px 20px;
border-bottom: 1px solid #2a2a2a;
flex-shrink: 0;
}
.presets-bar label {
font-size: 10px;
color: #666;
letter-spacing: 0.1em;
display: block;
margin-bottom: 8px;
}
.preset-chips {
display: flex;
gap: 6px;
}
.preset-chip {
background: transparent;
border: 1px solid #333;
color: #888;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
padding: 4px 10px;
cursor: pointer;
transition: all 0.15s;
}
.preset-chip:hover, .preset-chip.active {
border-color: #4af626;
color: #4af626;
}
.sidebar-scroll {
flex: 1;
overflow-y: auto;
padding: 12px 20px 40px;
}
.sidebar-scroll::-webkit-scrollbar { width: 6px; }
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
.section-title {
font-size: 10px;
font-weight: 700;
color: #555;
letter-spacing: 0.15em;
margin: 16px 0 10px;
padding-bottom: 6px;
border-bottom: 1px solid #222;
}
.section-title:first-child { margin-top: 0; }
.color-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
padding: 4px 0;
}
.color-row label {
font-size: 11px;
color: #888;
min-width: 140px;
flex-shrink: 0;
}
.color-row input[type="color"] {
width: 28px;
height: 28px;
border: 1px solid #333;
border-radius: 4px;
padding: 2px;
background: #1a1a1a;
cursor: pointer;
flex-shrink: 0;
}
.color-row input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.color-row input[type="color"]::-webkit-color-swatch { border: none; border-radius: 2px; }
.color-row input[type="text"] {
flex: 1;
min-width: 0;
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #ccc;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 5px 8px;
outline: none;
transition: border-color 0.15s;
}
.color-row input[type="text"]:focus {
border-color: #4af626;
}
/* ===== Preview Area ===== */
.preview-area {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #0a0a0a;
}
.preview-area::-webkit-scrollbar { width: 8px; }
.preview-area::-webkit-scrollbar-track { background: #111; }
.preview-area::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
.preview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: 16px;
}
.preview-grid .col-terminal {
grid-column: 1 / -1;
}
.preview-section {
min-width: 0;
}
.preview-label {
font-size: 10px;
color: #555;
letter-spacing: 0.15em;
margin-bottom: 8px;
font-weight: 700;
}
.preview-frame {
border-radius: 8px;
overflow: hidden;
border: 1px solid #222;
}
/* ===== Theme Variables (applied to preview) ===== */
.theme-scope {
--bg: #0a0a0a;
--bg-surface: #121212;
--bg-elevated: #131313;
--bg-terminal: #0e0e0e;
--text: #4af626;
--text-muted: #bbccb0;
--text-dim: #3c4b36;
--accent: #4af626;
--accent-secondary: #14d1ff;
--accent-secondary-light: #b7eaff;
--border: #1f1f1f;
--border-strong: #3c4b36;
--error: #ffb4ab;
--error-bg: #93000a;
--on-accent: #022100;
--surface-dot: #353534;
--scanline-tint: rgba(74, 246, 38, 0.02);
--accent-glow: rgba(74, 246, 38, 0.08);
--modal-overlay: rgba(0, 0, 0, 0.8);
}
/* ===== Login Preview ===== */
.login-preview {
background: var(--bg);
padding: 24px 16px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.login-preview .scanlines {
position: absolute; inset: 0;
background: linear-gradient(to bottom, transparent, transparent 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1));
background-size: 100% 4px;
pointer-events: none; z-index: 1;
}
.login-preview .flicker {
position: absolute; inset: 0;
background: var(--scanline-tint);
pointer-events: none; z-index: 1;
}
.login-card {
width: 100%;
max-width: 380px;
position: relative;
z-index: 2;
}
.login-title {
text-align: center;
margin-bottom: 24px;
}
.login-title .logo {
font-size: 24px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.05em;
}
.login-title .cursor {
display: inline-block;
width: 10px;
height: 20px;
background: var(--accent-secondary);
margin-left: 2px;
vertical-align: middle;
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.login-box {
background: var(--bg-surface);
border: 1px solid var(--border);
padding: 20px;
position: relative;
}
.login-box .top-bar {
position: absolute; top: 0; left: 0; right: 0;
height: 3px;
background: linear-gradient(to right, transparent, var(--accent), transparent);
opacity: 0.5;
}
.login-box .header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-strong);
}
.login-box .header span {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--accent-secondary);
}
.login-field {
margin-bottom: 14px;
}
.login-field label {
display: block;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--text-muted);
margin-bottom: 6px;
}
.login-field .input-line {
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-strong);
padding-bottom: 6px;
}
.login-field .input-line .prefix {
color: var(--text-muted);
margin-right: 8px;
font-size: 12px;
}
.login-field .input-line .placeholder-text {
color: var(--text-dim);
font-size: 12px;
}
.login-field .input-line:focus-within {
border-bottom-color: var(--accent);
}
.auth-tabs {
display: flex;
gap: 6px;
margin-bottom: 12px;
}
.auth-tab {
padding: 3px 10px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
border: 1px solid var(--border-strong);
color: var(--text-muted);
background: transparent;
font-family: 'JetBrains Mono', monospace;
}
.auth-tab.active {
background: var(--accent);
color: var(--on-accent);
border-color: var(--accent);
}
.connect-btn {
width: 100%;
padding: 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
background: var(--accent);
color: var(--on-accent);
border: 1px solid var(--accent);
cursor: pointer;
font-family: 'JetBrains Mono', monospace;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 16px;
transition: all 0.2s;
}
.connect-btn:hover {
background: var(--accent-secondary-light);
border-color: var(--accent-secondary-light);
color: var(--bg);
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
font-size: 11px;
}
.status-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--surface-dot);
margin-right: 4px;
vertical-align: middle;
}
.status-text { color: var(--text-muted); }
.login-footer {
text-align: center;
margin-top: 20px;
font-size: 11px;
color: var(--text-muted);
opacity: 0.6;
}
.login-footer a {
color: var(--accent);
text-decoration: none;
opacity: 0.6;
letter-spacing: 0.1em;
text-transform: uppercase;
}
/* ===== Terminal Preview ===== */
.terminal-preview {
background: var(--bg-terminal);
padding: 16px;
min-height: 240px;
font-size: 12px;
line-height: 1.6;
position: relative;
overflow: hidden;
}
.terminal-preview .scanline {
position: absolute; inset: 0;
background: linear-gradient(to bottom, transparent, transparent 50%, rgba(0,0,0,0.08) 50%, rgba(0,0,0,0.08));
background-size: 100% 4px;
pointer-events: none; z-index: 1;
opacity: 0.3;
}
.terminal-statusbar {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
color: var(--on-surface-variant);
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid var(--border);
opacity: 0.8;
position: relative;
z-index: 2;
}
.terminal-statusbar .connected {
display: flex;
align-items: center;
gap: 4px;
}
.terminal-statusbar .dot-green {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent);
}
.terminal-body {
position: relative;
z-index: 2;
}
.terminal-body .line { margin-bottom: 2px; }
.terminal-body .green { color: var(--accent); }
.terminal-body .cyan { color: var(--accent-secondary); }
.terminal-body .yellow { color: #e6db74; }
.terminal-body .red { color: var(--error); }
.terminal-body .gray { color: var(--text-dim); }
.terminal-body .white { color: var(--text); }
.terminal-body .prompt { color: var(--accent); font-weight: 600; }
.terminal-body .path { color: var(--accent-secondary); }
.terminal-input-mock {
display: flex;
align-items: center;
margin-top: 6px;
position: relative;
z-index: 2;
}
.terminal-input-mock .prompt-sign {
color: var(--accent);
margin-right: 6px;
font-weight: 600;
}
.terminal-input-mock .cursor-block {
display: inline-block;
width: 8px;
height: 14px;
background: var(--accent-secondary);
animation: blink 1s step-end infinite;
vertical-align: middle;
}
/* ===== User Space Preview ===== */
.userspace-preview {
background: var(--bg-elevated);
display: flex;
flex-direction: column;
min-height: 100%;
}
.us-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
height: 48px;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border-strong);
flex-shrink: 0;
}
.us-header .logo {
font-size: 16px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.05em;
}
.us-header .user-area {
display: flex;
align-items: center;
gap: 10px;
}
.us-header .avatar {
width: 26px;
height: 26px;
border-radius: 50%;
border: 2px solid var(--accent);
background: var(--bg-surface);
}
.us-header .username {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--text-muted);
}
.us-header .logout-icon {
color: var(--error);
font-size: 16px;
}
.us-body {
flex: 1;
padding: 16px;
position: relative;
overflow: hidden;
}
.us-body .scanlines {
position: absolute; inset: 0;
background: var(--modal-overlay);
pointer-events: none;
}
.us-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
position: relative;
z-index: 1;
}
.us-section-header h2 {
font-size: 13px;
font-weight: 700;
color: var(--accent);
letter-spacing: 0.1em;
}
.us-section-header .sub {
font-size: 10px;
color: var(--text-muted);
opacity: 0.6;
margin-top: 2px;
}
.us-section-header .add-btn {
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
padding: 4px 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.us-cards-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
position: relative;
z-index: 1;
}
.us-footer {
padding: 0 16px;
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--border-strong);
font-size: 9px;
color: var(--on-surface-variant);
opacity: 0.6;
flex-shrink: 0;
background: var(--bg-elevated);
}
/* ===== Server Cards Preview ===== */
.cards-preview {
background: var(--bg);
padding: 0;
}
.cards-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.cards-header h2 {
font-size: 14px;
font-weight: 700;
color: var(--accent);
letter-spacing: 0.1em;
}
.cards-header .add-btn {
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
padding: 5px 12px;
cursor: pointer;
}
.cards-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.server-card-mock {
background: var(--bg-surface);
border: 1px solid var(--border);
padding: 14px;
position: relative;
transition: border-color 0.2s;
}
.server-card-mock .top-line {
position: absolute; top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(to right, transparent, var(--border-strong), transparent);
}
.server-card-mock:hover {
border-color: var(--accent);
box-shadow: 0 4px 20px var(--accent-glow);
}
.server-card-mock:hover .top-line {
background: linear-gradient(to right, transparent, var(--accent), transparent);
}
.card-title-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.card-title-row .icon {
color: var(--accent);
font-size: 16px;
}
.card-title-row h3 {
font-size: 12px;
font-weight: 700;
color: var(--accent);
letter-spacing: 0.05em;
}
.card-badge {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--text-muted);
border: 1px solid var(--border-strong);
padding: 2px 6px;
margin-left: auto;
}
.card-info {
font-size: 11px;
color: var(--text-muted);
margin-bottom: 10px;
}
.card-info div {
display: flex;
gap: 6px;
margin-bottom: 3px;
}
.card-info .label { color: var(--text-dim); }
.card-info .value { color: var(--on-surface); }
.card-actions {
display: flex;
gap: 6px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.card-btn {
flex: 1;
padding: 4px 8px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
cursor: pointer;
text-transform: uppercase;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.card-btn .material-symbols-outlined { font-size: 12px; }
.card-btn.delete {
border-color: var(--error);
color: var(--error);
}
.card-btn.delete:hover {
background: var(--error-bg);
color: var(--error);
}
/* ===== Terminal + SFTP Layout ===== */
.terminal-sftp-preview {
display: flex;
min-height: 280px;
}
.terminal-sftp-preview .terminal-preview {
flex: 1;
min-width: 0;
}
/* ===== SFTP Preview ===== */
.sftp-preview {
width: 320px;
min-width: 320px;
background: var(--bg-surface);
display: flex;
flex-direction: column;
border-left: 1px solid var(--border-strong);
}
.sftp-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
height: 36px;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border-strong);
flex-shrink: 0;
}
.sftp-header .title {
display: flex;
align-items: center;
gap: 6px;
}
.sftp-header .title .icon {
color: var(--accent);
font-size: 14px;
}
.sftp-header .title span {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--accent);
}
.sftp-header .close-icon {
color: var(--on-surface-variant);
font-size: 14px;
}
.sftp-toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
border-bottom: 1px solid var(--border-strong);
flex-shrink: 0;
}
.sftp-toolbar .nav-icon {
color: var(--on-surface-variant);
font-size: 14px;
padding: 2px;
}
.sftp-toolbar .path-input {
flex: 1;
background: transparent;
border: none;
border-bottom: 1px solid var(--border-strong);
color: var(--on-surface);
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
padding: 3px 6px;
outline: none;
}
.sftp-actions {
display: flex;
align-items: center;
gap: 2px;
padding: 4px 10px;
border-bottom: 1px solid var(--border-strong);
flex-shrink: 0;
}
.sftp-action-btn {
display: flex;
align-items: center;
gap: 3px;
padding: 3px 6px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--accent);
background: transparent;
border: none;
cursor: pointer;
font-family: 'JetBrains Mono', monospace;
}
.sftp-action-btn.secondary {
color: var(--accent-secondary);
}
.sftp-action-btn .material-symbols-outlined {
font-size: 12px;
}
.sftp-file-list {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.sftp-file-item {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
font-size: 11px;
color: var(--on-surface);
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.sftp-file-item:hover {
background: var(--surface-dot);
}
.sftp-file-item.selected {
background: var(--surface-dot);
}
.sftp-file-item .file-icon {
font-size: 14px;
flex-shrink: 0;
}
.sftp-file-item .file-icon.dir {
color: var(--accent);
}
.sftp-file-item .file-icon.file {
color: var(--on-surface-variant);
}
.sftp-file-item .file-icon.link {
color: var(--accent-secondary);
}
.sftp-file-item .file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sftp-file-item .file-name.dir {
color: var(--accent);
font-weight: 700;
}
.sftp-file-item .file-size {
color: var(--on-surface-variant);
font-size: 10px;
width: 60px;
text-align: right;
flex-shrink: 0;
}
.sftp-file-item .file-perms {
color: var(--on-surface-variant);
font-size: 9px;
width: 70px;
text-align: right;
flex-shrink: 0;
}
.sftp-file-item .file-date {
color: var(--on-surface-variant);
font-size: 9px;
width: 80px;
text-align: right;
flex-shrink: 0;
}
.sftp-statusbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 12px;
background: var(--bg-elevated);
border-top: 1px solid var(--border-strong);
font-size: 9px;
color: var(--on-surface-variant);
flex-shrink: 0;
}
/* ===== Tab Bar Preview ===== */
.preview-tab-bar {
display: flex;
align-items: center;
background: var(--bg);
border-bottom: 1px solid var(--border);
padding: 0 8px;
height: 32px;
overflow-x: auto;
gap: 2px;
position: relative;
z-index: 2;
}
.preview-tab-item {
display: flex;
align-items: center;
gap: 5px;
padding: 3px 8px;
font-size: 10px;
font-weight: 600;
color: var(--text-muted);
background: transparent;
border: 1px solid transparent;
border-bottom: none;
border-radius: 4px 4px 0 0;
cursor: pointer;
white-space: nowrap;
position: relative;
}
.preview-tab-item:hover {
color: var(--text);
background: var(--bg-elevated);
border-color: var(--border);
}
.preview-tab-item.active {
color: var(--accent);
background: var(--bg-elevated);
border-color: var(--border-strong);
}
.preview-tab-item.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
}
.preview-tab-item.disconnected {
opacity: 0.5;
}
.preview-tab-item.disconnected .tab-label {
text-decoration: line-through;
}
.preview-tab-dot {
width: 5px;
height: 5px;
border-radius: 50%;
flex-shrink: 0;
}
.preview-tab-dot.connected {
background: var(--accent);
box-shadow: 0 0 3px var(--accent);
}
.preview-tab-dot.disconnected {
background: var(--text-dim);
}
.preview-tab-close {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-dim);
cursor: pointer;
opacity: 0;
transition: all 0.15s;
}
.preview-tab-item:hover .preview-tab-close,
.preview-tab-item.active .preview-tab-close {
opacity: 1;
}
.preview-tab-close:hover {
color: var(--error);
}
.preview-tab-new-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
border: 1px dashed var(--border);
background: transparent;
color: var(--text-dim);
border-radius: 3px;
cursor: pointer;
margin-left: 2px;
}
.preview-tab-new-btn:hover {
color: var(--accent);
border-color: var(--accent);
background: var(--bg-elevated);
}
/* ===== Agent Panel Preview ===== */
.preview-grid .col-agent {
grid-column: 1 / -1;
}
.agent-preview {
background: var(--bg);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
height: 100%;
min-height: 260px;
}
.agent-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
background: var(--bg-elevated);
}
.agent-header-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--accent-secondary);
}
.agent-close-icon {
font-size: 14px;
color: var(--text-muted);
cursor: pointer;
}
.agent-messages {
flex: 1;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
}
/* User message bubble */
.agent-msg-user {
display: flex;
justify-content: flex-end;
gap: 6px;
align-items: flex-start;
}
.agent-msg-user .agent-msg-text {
background: color-mix(in srgb, var(--agent-user-color) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--agent-user-color) 30%, transparent);
padding: 6px 10px;
border-radius: 6px;
font-size: 11px;
color: var(--agent-user-color);
max-width: 70%;
}
.agent-icon-user {
font-size: 14px;
color: var(--agent-user-color);
margin-top: 2px;
flex-shrink: 0;
}
/* Agent message */
.agent-msg-agent {
display: flex;
gap: 8px;
align-items: flex-start;
}
.agent-icon-bot {
font-size: 14px;
color: var(--agent-agent-color);
margin-top: 2px;
flex-shrink: 0;
}
.agent-response-body {
font-size: 11px;
color: var(--on-surface);
line-height: 1.5;
}
.agent-response-body h3 {
font-size: 12px;
font-weight: 700;
color: var(--agent-agent-color);
margin-bottom: 6px;
}
.agent-response-body p {
margin-top: 6px;
}
.agent-table {
border-collapse: collapse;
width: 100%;
font-size: 10px;
margin: 4px 0;
}
.agent-table th,
.agent-table td {
padding: 3px 8px;
border-bottom: 1px solid color-mix(in srgb, var(--agent-agent-color) 20%, transparent);
text-align: left;
}
.agent-table th {
font-weight: 700;
color: var(--agent-agent-color);
background: color-mix(in srgb, var(--agent-agent-color) 10%, transparent);
}
.agent-code {
background: color-mix(in srgb, var(--agent-agent-color) 10%, transparent);
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;
}
/* Thinking Process (collapsed) */
.tp-container {
border: 1px solid color-mix(in srgb, var(--agent-agent-color) 20%, transparent);
border-radius: 6px;
overflow: hidden;
}
.tp-header {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 10px;
background: color-mix(in srgb, var(--agent-agent-color) 6%, transparent);
font-size: 10px;
}
.tp-header .tp-chevron {
font-size: 12px;
color: var(--text-muted);
}
.tp-header .tp-icon {
font-size: 14px;
color: var(--agent-agent-color);
}
.tp-status-text {
color: var(--on-surface-variant);
font-weight: 600;
}
/* Input bar */
.agent-input-bar {
display: flex;
gap: 8px;
align-items: flex-end;
padding: 8px 12px;
border-top: 1px solid var(--border);
background: var(--bg-elevated);
}
.agent-input {
flex: 1;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
font-size: 11px;
color: var(--on-surface);
resize: none;
font-family: inherit;
min-height: 32px;
}
.agent-input::placeholder {
color: var(--on-surface-variant);
opacity: 0.7;
}
.agent-send-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: var(--accent);
color: var(--on-accent);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
}
/* ===== Hidden file input ===== */
#import-input { display: none; }
</style>
</head>
<body>
<div class="editor-layout">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h1>THEME_EDITOR</h1>
<div class="sidebar-actions">
<button id="btn-import" title="Import JSON">IMPORT</button>
<button id="btn-export" class="primary" title="Export JSON">EXPORT</button>
</div>
</div>
<div class="presets-bar">
<label>PRESETS</label>
<div class="preset-chips">
<button class="preset-chip active" data-preset="cyberpunk">Cyberpunk</button>
<button class="preset-chip" data-preset="glacier">Glacier</button>
<button class="preset-chip" data-preset="gruvbox">Gruvbox</button>
</div>
</div>
<div class="sidebar-scroll" id="controls"></div>
</div>
<!-- Preview -->
<div class="preview-area">
<div class="theme-scope" id="theme-scope">
<div class="preview-grid">
<!-- Login Page (Top Left) -->
<div class="preview-section">
<div class="preview-label">LOGIN PAGE</div>
<div class="preview-frame">
<div class="login-preview">
<div class="scanlines"></div>
<div class="flicker"></div>
<div class="login-card">
<div class="login-title">
<span class="logo">CloudSSH</span><span class="cursor"></span>
</div>
<div class="login-box">
<div class="top-bar"></div>
<div class="header">
<span>CONNECTION_PARAMETERS</span>
<span class="material-symbols-outlined" style="font-size:14px;color:var(--accent-secondary);font-variation-settings:'FILL' 0">terminal</span>
</div>
<div style="display:grid;grid-template-columns:3fr 1fr;gap:12px;margin-bottom:12px">
<div class="login-field" style="margin-bottom:0">
<label>HOST_ADDRESS</label>
<div class="input-line">
<span class="prefix">&gt;</span>
<span class="placeholder-text">192.168.1.1</span>
</div>
</div>
<div class="login-field" style="margin-bottom:0">
<label>PORT</label>
<div class="input-line">
<span class="prefix">:</span>
<span class="placeholder-text">22</span>
</div>
</div>
</div>
<div class="login-field">
<label>AUTH_USER</label>
<div class="input-line">
<span class="material-symbols-outlined prefix" style="font-size:14px">person</span>
<span class="placeholder-text">admin</span>
</div>
</div>
<div class="login-field">
<label>AUTH_METHOD</label>
<div class="auth-tabs">
<span class="auth-tab active">PASSWORD</span>
<span class="auth-tab">PRIVATE_KEY</span>
</div>
<div class="input-line" style="border-bottom:1px solid var(--border-strong);padding-bottom:6px">
<span class="material-symbols-outlined prefix" style="font-size:14px">key</span>
<span class="placeholder-text">••••••••</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:6px;margin-top:8px;margin-bottom:4px">
<span style="width:14px;height:14px;border:1px solid var(--border-strong);border-radius:2px;display:inline-block;background:var(--bg-surface)"></span>
<label style="font-size:10px;color:var(--text-muted);cursor:pointer">REMEMBER_CONNECTION</label>
</div>
<button class="connect-btn" style="margin-top:12px">
<span class="material-symbols-outlined" style="font-size:14px">power_settings_new</span>
Execute_Connection
</button>
<div class="status-bar">
<span class="status-text"><span class="status-dot"></span> STATUS: OFFLINE</span>
<span style="font-size:10px;color:var(--text-muted);opacity:0.5">GitHub</span>
</div>
<!-- Recent Connections Preview -->
<div style="margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); display: block;">
<label style="display: block; font-size: 10px; font-weight: 700; letter-spacing: 0.1em; color: var(--accent-secondary); margin-bottom: 8px;">RECENT_CONNECTIONS</label>
<div style="display: flex; flex-direction: column; gap: 6px;">
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px; padding: 6px 8px; border: 1px solid var(--border); background: rgba(255,255,255,0.02); cursor: pointer;">
<div style="display: flex; align-items: center; gap: 6px; overflow: hidden; margin-right: 6px;">
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-muted)">history</span>
<span style="color: var(--on-surface); text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">admin@192.168.1.1:22</span>
<span style="font-size: 8px; font-weight: 700; letter-spacing: 0.05em; color: var(--text-muted); border: 1px solid var(--border-strong); padding: 1px 3px;">PWD</span>
</div>
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-dim)">close</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px; padding: 6px 8px; border: 1px solid var(--border); background: rgba(255,255,255,0.02); cursor: pointer;">
<div style="display: flex; align-items: center; gap: 6px; overflow: hidden; margin-right: 6px;">
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-muted)">history</span>
<span style="color: var(--on-surface); text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">deploy@10.0.0.5:2222</span>
<span style="font-size: 8px; font-weight: 700; letter-spacing: 0.05em; color: var(--text-muted); border: 1px solid var(--border-strong); padding: 1px 3px;">KEY</span>
</div>
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-dim)">close</span>
</div>
</div>
</div>
</div>
<div class="login-footer">
<a href="#">[ GitHub Open Source ]</a>
</div>
</div>
</div>
</div>
</div>
<!-- User Space (Top Right) -->
<div class="preview-section">
<div class="preview-label">USER SPACE</div>
<div class="preview-frame">
<div class="userspace-preview">
<div class="us-header">
<span class="logo">CloudSSH</span>
<div class="user-area">
<div class="avatar"></div>
<span class="username">admin</span>
<span class="material-symbols-outlined logout-icon" style="font-variation-settings:'FILL' 0">logout</span>
</div>
</div>
<div class="us-body">
<div class="scanlines"></div>
<div class="us-section-header">
<div>
<h2>SERVER_LIST</h2>
<div class="sub">Manage your SSH connections</div>
</div>
<span class="add-btn">
<span class="material-symbols-outlined" style="font-size:13px">add</span>
ADD_SERVER
</span>
</div>
<div class="us-cards-grid">
<div class="server-card-mock">
<div class="top-line"></div>
<div class="card-title-row">
<span class="material-symbols-outlined icon" style="font-variation-settings:'FILL' 0">dns</span>
<h3>Production</h3>
<span class="card-badge">PWD</span>
</div>
<div class="card-info">
<div><span class="label">HOST</span><span class="value">192.168.1.10:22</span></div>
<div><span class="label">USER</span><span class="value">root</span></div>
</div>
<div class="card-actions">
<span class="card-btn"><span class="material-symbols-outlined">power_settings_new</span> CONNECT</span>
<span class="card-btn"><span class="material-symbols-outlined">edit</span></span>
<span class="card-btn delete"><span class="material-symbols-outlined">delete</span></span>
</div>
</div>
<div class="server-card-mock">
<div class="top-line"></div>
<div class="card-title-row">
<span class="material-symbols-outlined icon" style="font-variation-settings:'FILL' 0">dns</span>
<h3>Staging</h3>
<span class="card-badge">KEY</span>
</div>
<div class="card-info">
<div><span class="label">HOST</span><span class="value">10.0.0.5:2222</span></div>
<div><span class="label">USER</span><span class="value">deploy</span></div>
</div>
<div class="card-actions">
<span class="card-btn"><span class="material-symbols-outlined">power_settings_new</span> CONNECT</span>
<span class="card-btn"><span class="material-symbols-outlined">edit</span></span>
<span class="card-btn delete"><span class="material-symbols-outlined">delete</span></span>
</div>
</div>
</div>
</div>
<div class="us-footer">
<span>© 2025 CLOUD_SSH_TERMINAL</span>
<span>Documentation</span>
</div>
</div>
</div>
</div>
<!-- Terminal + SFTP (Bottom, Full Width) -->
<div class="preview-section col-terminal">
<div class="preview-label">TERMINAL + SFTP</div>
<div class="preview-frame">
<!-- Tab Bar Preview -->
<div class="preview-tab-bar">
<div class="preview-tab-item active">
<span class="preview-tab-dot connected"></span>
<span class="tab-label">root@ubuntu</span>
<span class="material-symbols-outlined preview-tab-close" style="font-size:12px">close</span>
</div>
<div class="preview-tab-item">
<span class="preview-tab-dot connected"></span>
<span class="tab-label">Production</span>
<span class="material-symbols-outlined preview-tab-close" style="font-size:12px">close</span>
</div>
<div class="preview-tab-item disconnected">
<span class="preview-tab-dot disconnected"></span>
<span class="tab-label">10.0.0.5</span>
<span class="material-symbols-outlined preview-tab-close" style="font-size:12px">close</span>
</div>
<div class="preview-tab-new-btn">
<span class="material-symbols-outlined" style="font-size:12px">add</span>
</div>
</div>
<div class="terminal-sftp-preview">
<!-- Terminal Area -->
<div class="terminal-preview">
<div class="scanline"></div>
<div class="terminal-statusbar" style="display:flex;justify-content:space-between;">
<div style="display:flex;gap:20px;">
<span class="connected"><span class="dot-green"></span> Connected</span>
<span>Host: 192.168.1.1</span>
<span>User: root</span>
<span>Port: 22</span>
</div>
<div>
<span>⚡ CF-HKG: 34ms | RTT: 12ms</span>
</div>
</div>
<div class="terminal-body">
<div class="line green">[+] WebSocket connected, sending credentials...</div>
<div class="line green">[*] Authenticating...</div>
<div class="line green">[*] Authentication successful</div>
<div class="line white">Welcome to Ubuntu 22.04.3 LTS</div>
<div class="line white"> </div>
<div class="line"><span class="prompt">root@server</span><span class="white">:</span><span class="path">~</span><span class="white">$ ls -la</span></div>
<div class="line white">drwxr-xr-x 5 root root 4096 Jan 15 10:23 .</div>
<div class="line white">drwxr-xr-x 3 root root 4096 Jan 15 10:20 ..</div>
<div class="line cyan">-rw-r--r-- 1 root root 220 Jan 15 10:20 .bashrc</div>
<div class="line cyan">drwxr-xr-x 2 root root 4096 Jan 15 10:23 config</div>
<div class="line cyan">-rwxr-xr-x 1 root root 1024 Jan 15 10:23 deploy.sh</div>
<div class="line yellow">-rw-r--r-- 1 root root 512 Jan 15 10:22 README.md</div>
<div class="line white"> </div>
<div class="line"><span class="prompt">root@server</span><span class="white">:</span><span class="path">~</span><span class="white">$ </span><span class="cursor-block"></span></div>
</div>
</div>
<!-- SFTP Panel (Right Side) -->
<div class="sftp-preview">
<div class="sftp-header">
<div class="title">
<span class="material-symbols-outlined icon" style="font-variation-settings:'FILL' 1">folder_open</span>
<span>SFTP_FILE_MANAGER</span>
</div>
<span class="material-symbols-outlined close-icon">close</span>
</div>
<div class="sftp-toolbar">
<span class="material-symbols-outlined nav-icon">arrow_back</span>
<span class="material-symbols-outlined nav-icon">home</span>
<span class="material-symbols-outlined nav-icon">refresh</span>
<input class="path-input" value="/home/admin" readonly>
<span class="material-symbols-outlined nav-icon" style="color:var(--accent)">arrow_forward</span>
</div>
<div class="sftp-actions">
<button class="sftp-action-btn"><span class="material-symbols-outlined">upload_file</span>UPLOAD</button>
<button class="sftp-action-btn secondary"><span class="material-symbols-outlined">create_new_folder</span>MKDIR</button>
<button class="sftp-action-btn" style="opacity:0.3"><span class="material-symbols-outlined">download</span>DOWNLOAD</button>
<button class="sftp-action-btn" style="opacity:0.3;color:var(--error)"><span class="material-symbols-outlined">delete</span>DELETE</button>
</div>
<div class="sftp-file-list">
<div class="sftp-file-item">
<span class="material-symbols-outlined file-icon dir" style="font-variation-settings:'FILL' 1">folder</span>
<span class="file-name dir">.ssh</span>
<span class="file-size">-</span>
<span class="file-perms">drwx------</span>
<span class="file-date">Jan 10 09:15</span>
</div>
<div class="sftp-file-item">
<span class="material-symbols-outlined file-icon dir" style="font-variation-settings:'FILL' 1">folder</span>
<span class="file-name dir">projects</span>
<span class="file-size">-</span>
<span class="file-perms">drwxr-xr-x</span>
<span class="file-date">Jan 14 16:30</span>
</div>
<div class="sftp-file-item selected">
<span class="material-symbols-outlined file-icon dir" style="font-variation-settings:'FILL' 1">folder</span>
<span class="file-name dir">config</span>
<span class="file-size">-</span>
<span class="file-perms">drwxr-xr-x</span>
<span class="file-date">Jan 15 10:23</span>
</div>
<div class="sftp-file-item">
<span class="material-symbols-outlined file-icon file">draft</span>
<span class="file-name">.bashrc</span>
<span class="file-size">4.2 KB</span>
<span class="file-perms">-rw-r--r--</span>
<span class="file-date">Jan 10 09:00</span>
</div>
<div class="sftp-file-item">
<span class="material-symbols-outlined file-icon file">terminal</span>
<span class="file-name">deploy.sh</span>
<span class="file-size">1.0 KB</span>
<span class="file-perms">-rwxr-xr-x</span>
<span class="file-date">Jan 15 10:23</span>
</div>
<div class="sftp-file-item">
<span class="material-symbols-outlined file-icon file">description</span>
<span class="file-name">README.md</span>
<span class="file-size">512 B</span>
<span class="file-perms">-rw-r--r--</span>
<span class="file-date">Jan 12 14:20</span>
</div>
<div class="sftp-file-item">
<span class="material-symbols-outlined file-icon link">link</span>
<span class="file-name">logs -> /var/log/app</span>
<span class="file-size">-</span>
<span class="file-perms">lrwxrwxrwx</span>
<span class="file-date">Jan 08 11:00</span>
</div>
</div>
<div class="sftp-statusbar">
<span>Ready</span>
<span>2 dirs, 4 files</span>
</div>
</div>
</div>
</div>
</div>
<!-- AI Agent Panel (Below, Full Width) -->
<div class="preview-section col-agent">
<div class="preview-label">AI AGENT PANEL (THINKING PROGRESS)</div>
<div class="preview-frame">
<div class="agent-preview">
<!-- Agent Panel Header -->
<div class="agent-header">
<span class="agent-header-title">AI_AGENT</span>
<span class="material-symbols-outlined agent-close-icon">close</span>
</div>
<!-- Message List -->
<div class="agent-messages">
<!-- User Message -->
<div class="agent-msg-user">
<span class="agent-msg-text">帮我检查服务器的配置信息</span>
<span class="material-symbols-outlined agent-icon-user" style="font-variation-settings:'FILL' 1">person</span>
</div>
<!-- Thinking Process Container (Collapsed) -->
<div class="tp-container">
<div class="tp-header">
<span class="material-symbols-outlined tp-chevron">expand_more</span>
<span class="material-symbols-outlined tp-icon" style="font-variation-settings:'FILL' 1">check_circle</span>
<span class="tp-status-text">已完成 5 个步骤</span>
</div>
</div>
<!-- Agent Response (Markdown) -->
<div class="agent-msg-agent">
<span class="material-symbols-outlined agent-icon-bot" style="font-variation-settings:'FILL' 1">smart_toy</span>
<div class="agent-response-body">
<h3>服务器配置信息汇总</h3>
<table class="agent-table">
<tr><th>项目</th><th>值</th></tr>
<tr><td>操作系统</td><td>Ubuntu 24.04.4 LTS</td></tr>
<tr><td>内核版本</td><td>Linux 6.8.0 x86_64</td></tr>
<tr><td>CPU</td><td>1 vCPU @ 2494 MHz</td></tr>
<tr><td>内存</td><td>512 MiB (45% used)</td></tr>
</table>
<p>建议定期执行 <code class="agent-code">apt update && apt upgrade</code></p>
</div>
</div>
</div>
<!-- Input Area -->
<div class="agent-input-bar">
<textarea class="agent-input" placeholder="描述你想做的事..." readonly>描述你想做的事...</textarea>
<button class="agent-send-btn">
<span class="material-symbols-outlined" style="font-size:14px">arrow_upward</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<input type="file" id="import-input" accept=".json">
<script>
const PRESETS = {
cyberpunk: {
name: 'Cyberpunk',
terminal: { background: '#0a0a0a', foreground: '#4af626', cursor: '#14d1ff', cursorAccent: '#0a0a0a', selectionBackground: '#273747' },
ui: {
'--bg': '#0a0a0a', '--bg-surface': '#121212', '--bg-elevated': '#131313', '--bg-terminal': '#0e0e0e',
'--text': '#4af626', '--text-muted': '#bbccb0', '--text-dim': '#3c4b36',
'--accent': '#4af626', '--accent-secondary': '#14d1ff', '--accent-secondary-light': '#b7eaff',
'--border': '#1f1f1f', '--border-strong': '#3c4b36',
'--error': '#ffb4ab', '--error-bg': '#93000a',
'--on-accent': '#022100', '--surface-dot': '#353534',
'--scanline-tint': 'rgba(74, 246, 38, 0.02)',
'--accent-glow': 'rgba(74, 246, 38, 0.08)',
'--modal-overlay': 'rgba(0, 0, 0, 0.8)',
'--on-surface': '#e5e2e1', '--on-surface-variant': '#bbccb0',
'--agent-user-color': '#4af626', '--agent-agent-color': '#14d1ff',
}
},
glacier: {
name: 'Glacier',
terminal: { background: '#0a192f', foreground: '#64ffda', cursor: '#e6f1ff', cursorAccent: '#0a192f', selectionBackground: '#112240' },
ui: {
'--bg': '#0a192f', '--bg-surface': '#0d2137', '--bg-elevated': '#112240', '--bg-terminal': '#061526',
'--text': '#64ffda', '--text-muted': '#8892b0', '--text-dim': '#495670',
'--accent': '#64ffda', '--accent-secondary': '#e6f1ff', '--accent-secondary-light': '#ccd6f6',
'--border': '#1d3557', '--border-strong': '#495670',
'--error': '#ff6b6b', '--error-bg': '#3d0000',
'--on-accent': '#0a192f', '--surface-dot': '#1d3557',
'--scanline-tint': 'rgba(100, 255, 218, 0.02)',
'--accent-glow': 'rgba(100, 255, 218, 0.08)',
'--modal-overlay': 'rgba(0, 0, 0, 0.85)',
'--on-surface': '#e6f1ff', '--on-surface-variant': '#8892b0',
'--agent-user-color': '#64ffda', '--agent-agent-color': '#e6f1ff',
}
},
gruvbox: {
name: 'Gruvbox',
terminal: { background: '#282828', foreground: '#ebdbb2', cursor: '#d3869b', cursorAccent: '#282828', selectionBackground: '#504945' },
ui: {
'--bg': '#282828', '--bg-surface': '#303030', '--bg-elevated': '#282828', '--bg-terminal': '#1d2021',
'--text': '#ebdbb2', '--text-muted': '#a89984', '--text-dim': '#665c54',
'--accent': '#b8bb26', '--accent-secondary': '#83a598', '--accent-secondary-light': '#8ec07c',
'--border': '#3c3836', '--border-strong': '#665c54',
'--error': '#fb4934', '--error-bg': '#3d0000',
'--on-accent': '#282828', '--surface-dot': '#3c3836',
'--scanline-tint': 'rgba(184, 187, 38, 0.02)',
'--accent-glow': 'rgba(184, 187, 38, 0.08)',
'--modal-overlay': 'rgba(0, 0, 0, 0.75)',
'--on-surface': '#ebdbb2', '--on-surface-variant': '#a89984',
'--agent-user-color': '#b8bb26', '--agent-agent-color': '#83a598',
}
}
};
const VARIABLE_GROUPS = [
{ title: 'BACKGROUNDS', vars: ['--bg', '--bg-surface', '--bg-elevated', '--bg-terminal'] },
{ title: 'TEXT COLORS', vars: ['--text', '--text-muted', '--text-dim', '--on-surface', '--on-surface-variant'] },
{ title: 'ACCENT COLORS', vars: ['--accent', '--accent-secondary', '--accent-secondary-light', '--on-accent'] },
{ title: 'BORDERS', vars: ['--border', '--border-strong'] },
{ title: 'ERROR', vars: ['--error', '--error-bg'] },
{ title: 'COMPONENTS', vars: ['--surface-dot'] },
{ title: 'AI AGENT', vars: ['--agent-user-color', '--agent-agent-color'] },
{ title: 'EFFECTS', vars: ['--scanline-tint', '--accent-glow', '--modal-overlay'] },
];
const VARIABLE_LABELS = {
'--bg': 'Page Background',
'--bg-surface': 'Card / Surface',
'--bg-elevated': 'Elevated Surface',
'--bg-terminal': 'Terminal Background',
'--text': 'Primary Text',
'--text-muted': 'Muted Text',
'--text-dim': 'Dim Text',
'--accent': 'Primary Accent',
'--accent-secondary': 'Secondary Accent',
'--accent-secondary-light': 'Secondary Light',
'--on-accent': 'Text on Accent',
'--border': 'Border',
'--border-strong': 'Strong Border',
'--error': 'Error Text',
'--error-bg': 'Error Background',
'--surface-dot': 'Status Dot',
'--agent-user-color': 'Agent User Color',
'--agent-agent-color': 'Agent Bot Color',
'--scanline-tint': 'Scanline Overlay',
'--accent-glow': 'Accent Glow',
'--modal-overlay': 'Modal Overlay',
'--on-surface': 'Surface Text',
'--on-surface-variant': 'Surface Text Variant',
};
let theme = {};
let activePreset = 'cyberpunk';
let customized = false;
function initTheme(presetName) {
const preset = PRESETS[presetName];
theme = { ...preset.ui };
activePreset = presetName;
customized = false;
renderControls();
applyTheme();
document.querySelectorAll('.preset-chip').forEach(c => {
c.classList.toggle('active', c.dataset.preset === presetName);
});
}
function isHexColor(val) {
return /^#[0-9a-fA-F]{6}$/.test(val) || /^#[0-9a-fA-F]{3}$/.test(val);
}
function hexToRgba(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, 1)`;
}
function renderControls() {
const container = document.getElementById('controls');
let html = '';
for (const group of VARIABLE_GROUPS) {
html += `<div class="section-title">${group.title}</div>`;
for (const varName of group.vars) {
const val = theme[varName] || '';
const label = VARIABLE_LABELS[varName] || varName;
const isHex = isHexColor(val);
html += `<div class="color-row">`;
html += `<label>${label}</label>`;
if (isHex) {
html += `<input type="color" value="${val}" data-var="${varName}">`;
}
html += `<input type="text" value="${val}" data-var="${varName}" spellcheck="false">`;
html += `</div>`;
}
}
container.innerHTML = html;
// Bind events
container.querySelectorAll('input[type="color"]').forEach(input => {
input.addEventListener('input', (e) => {
const varName = e.target.dataset.var;
theme[varName] = e.target.value;
const textInput = container.querySelector(`input[type="text"][data-var="${varName}"]`);
if (textInput) textInput.value = e.target.value;
customized = true;
applyTheme();
});
});
container.querySelectorAll('input[type="text"]').forEach(input => {
input.addEventListener('input', (e) => {
const varName = e.target.dataset.var;
const val = e.target.value;
theme[varName] = val;
const colorInput = container.querySelector(`input[type="color"][data-var="${varName}"]`);
if (colorInput && isHexColor(val)) {
colorInput.value = val.length === 4
? '#' + val[1]+val[1]+val[2]+val[2]+val[3]+val[3]
: val;
}
customized = true;
applyTheme();
});
});
}
function applyTheme() {
const scope = document.getElementById('theme-scope');
for (const [varName, val] of Object.entries(theme)) {
scope.style.setProperty(varName, val);
}
}
// Preset chips
document.querySelectorAll('.preset-chip').forEach(chip => {
chip.addEventListener('click', () => initTheme(chip.dataset.preset));
});
// Import
document.getElementById('btn-import').addEventListener('click', () => {
document.getElementById('import-input').click();
});
document.getElementById('import-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const data = JSON.parse(ev.target.result);
if (data.ui && typeof data.ui === 'object') {
theme = { ...data.ui };
customized = false;
if (data.name) {
document.querySelector('.preset-chip.active')?.classList.remove('active');
activePreset = null;
}
renderControls();
applyTheme();
} else {
alert('Invalid theme file: missing "ui" field');
}
} catch {
alert('Invalid JSON file');
}
};
reader.readAsText(file);
e.target.value = '';
});
// Export
document.getElementById('btn-export').addEventListener('click', () => {
const isCustom = customized || !activePreset;
const preset = activePreset ? PRESETS[activePreset] : null;
const themeName = isCustom ? 'Custom' : (preset?.name || 'Custom');
const output = {
name: themeName,
terminal: (isCustom || !preset) ? {
background: theme['--bg-terminal'] || '#0a0a0a',
foreground: theme['--text'] || '#cccccc',
cursor: theme['--accent-secondary'] || '#ffffff',
cursorAccent: theme['--bg-terminal'] || '#0a0a0a',
selectionBackground: theme['--border-strong'] || '#333333',
} : preset.terminal,
ui: { ...theme },
};
const json = JSON.stringify(output, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `cloudssh-theme-${themeName.toLowerCase().replace(/\s+/g, '-')}.json`;
a.click();
URL.revokeObjectURL(url);
});
// Init
initTheme('cyberpunk');
</script>
</body>
</html>