hyperclaw 5.3.2 → 5.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/static/chat.html CHANGED
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" data-theme="dark">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -8,184 +8,364 @@
8
8
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
9
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
10
  <style>
11
+ /* ── ACCENT THEME VARS ── */
11
12
  :root {
12
- --bg: #090e1a;
13
- --bg2: #0d1526;
14
- --bg3: #111d33;
15
- --border: #1e2d4a;
16
- --accent: #00c2ff;
17
- --accent2: #0077b6;
18
- --text: #e2eaf8;
19
- --text2: #8ba0c0;
20
- --user-bg: linear-gradient(135deg, #0077b6, #00c2ff);
21
- --asst-bg: #111d33;
22
- --header-bg: rgba(9,14,26,0.95);
23
- --shadow: 0 4px 24px rgba(0,194,255,0.08);
13
+ --accent: #22d3ee;
14
+ --accent2: #06b6d4;
15
+ --accent-glow: rgba(34,211,238,0.3);
16
+ --accent-bg: rgba(34,211,238,0.08);
24
17
  }
25
18
  [data-theme="light"] {
26
- --bg: #eef4ff;
27
- --bg2: #dde8ff;
28
- --bg3: #ffffff;
29
- --border: #b8cdf0;
30
- --accent: #0077b6;
31
- --accent2: #005f99;
32
- --text: #0d1d3a;
33
- --text2: #3a5580;
34
- --user-bg: linear-gradient(135deg, #0077b6, #00aee0);
35
- --asst-bg: #ffffff;
36
- --header-bg: rgba(238,244,255,0.97);
37
- --shadow: 0 4px 24px rgba(0,119,182,0.10);
19
+ --accent: #0891b2;
20
+ --accent2: #0e7490;
21
+ --accent-glow: rgba(8,145,178,0.25);
22
+ --accent-bg: rgba(8,145,178,0.06);
38
23
  }
24
+ [data-daemon="true"] {
25
+ --accent: #f87171;
26
+ --accent2: #ef4444;
27
+ --accent-glow: rgba(248,113,113,0.3);
28
+ --accent-bg: rgba(248,113,113,0.08);
29
+ }
30
+ [data-theme="light"][data-daemon="true"] {
31
+ --accent: #dc2626;
32
+ --accent2: #b91c1c;
33
+ --accent-glow: rgba(220,38,38,0.25);
34
+ --accent-bg: rgba(220,38,38,0.06);
35
+ }
36
+
39
37
  * { box-sizing: border-box; margin: 0; padding: 0; }
40
- body { background: var(--bg); color: var(--text); font-family: 'Inter', system-ui, sans-serif; height: 100vh; display: flex; flex-direction: column; transition: background .3s, color .3s; }
41
-
42
- /* ── BANNER ─── */
43
- .banner-wrap { background: var(--bg2); border-bottom: 1px solid var(--border); padding: 14px 20px; display: flex; align-items: center; gap: 16px; box-shadow: var(--shadow); position: sticky; top: 0; z-index: 100; backdrop-filter: blur(8px); background: var(--header-bg); }
44
- .banner-logo { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: clamp(18px, 4vw, 28px); letter-spacing: 2px; background: linear-gradient(90deg, var(--accent), #7dd4fc, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-shadow: none; line-height: 1; user-select: none; }
45
- .banner-eagle { font-size: 28px; filter: drop-shadow(0 0 8px var(--accent)); }
46
- .banner-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
47
- .banner-sub { font-size: 11px; color: var(--text2); letter-spacing: 0.5px; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
48
- .banner-status { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; }
49
- .dot { width: 8px; height: 8px; border-radius: 50%; background: #ef4444; flex-shrink: 0; }
50
- .dot.connected { background: #22c55e; box-shadow: 0 0 6px #22c55e; animation: pulse-green 2s infinite; }
51
- @keyframes pulse-green { 0%,100%{box-shadow:0 0 4px #22c55e} 50%{box-shadow:0 0 12px #22c55e} }
52
- .banner-actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
53
- .btn-icon { background: var(--bg3); border: 1px solid var(--border); color: var(--text2); width: 34px; height: 34px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; transition: all .2s; }
54
- .btn-icon:hover { border-color: var(--accent); color: var(--accent); }
55
-
56
- /* ── MESSAGES ─── */
57
- #messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth; }
58
- #messages::-webkit-scrollbar { width: 4px; }
59
- #messages::-webkit-scrollbar-track { background: transparent; }
60
- #messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
61
- .msg-row { display: flex; gap: 10px; max-width: 800px; animation: fadeIn .25s ease; }
62
- @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
38
+ body { background: #0a0a0a; color: #e5e5e5; font-family: 'Inter', system-ui, sans-serif; height: 100vh; display: flex; overflow: hidden; }
39
+
40
+ /* ── SCROLLBAR ── */
41
+ ::-webkit-scrollbar { width: 4px; }
42
+ ::-webkit-scrollbar-track { background: transparent; }
43
+ ::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 2px; }
44
+
45
+ /* ── SIDEBAR ── */
46
+ #sidebar { width: 240px; flex-shrink: 0; background: #111111; border-right: 1px solid #1e1e1e; display: flex; flex-direction: column; height: 100vh; overflow: hidden; transition: width 0.2s; }
47
+ #sidebar.collapsed { width: 0; border: none; }
48
+
49
+ .sidebar-top { padding: 14px 12px 8px; flex-shrink: 0; }
50
+ .sidebar-logo { display: flex; align-items: center; gap: 9px; padding: 6px 8px; margin-bottom: 6px; }
51
+ .sidebar-logo img { width: 26px; height: 26px; border-radius: 6px; }
52
+ .sidebar-logo-text { font-size: 13px; font-weight: 600; color: #e5e5e5; }
53
+
54
+ .sidebar-btn { width: 100%; display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 8px; border: none; background: transparent; color: #a3a3a3; font-size: 13px; cursor: pointer; transition: background .15s, color .15s; text-align: left; white-space: nowrap; overflow: hidden; }
55
+ .sidebar-btn:hover { background: #1e1e1e; color: #e5e5e5; }
56
+ .sidebar-btn.active { background: #1e1e1e; color: #e5e5e5; }
57
+ .sidebar-btn svg { flex-shrink: 0; }
58
+
59
+ .sidebar-new-chat { width: calc(100% - 24px); margin: 0 12px 4px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 8px; border: 1px solid #2a2a2a; background: transparent; color: #e5e5e5; font-size: 13px; cursor: pointer; transition: background .15s, border-color .15s; font-family: inherit; }
60
+ .sidebar-new-chat:hover { background: #1e1e1e; border-color: #3a3a3a; }
61
+
62
+ .sidebar-search { margin: 4px 12px; position: relative; }
63
+ .sidebar-search input { width: 100%; background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 8px; padding: 7px 10px 7px 32px; font-size: 12px; color: #e5e5e5; outline: none; font-family: inherit; transition: border-color .15s; }
64
+ .sidebar-search input:focus { border-color: #3a3a3a; }
65
+ .sidebar-search input::placeholder { color: #555; }
66
+ .sidebar-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: #555; pointer-events: none; }
67
+
68
+ .sidebar-section { padding: 16px 12px 4px; font-size: 11px; font-weight: 500; color: #555; letter-spacing: 0.5px; text-transform: uppercase; }
69
+
70
+ .sidebar-project { width: calc(100% - 24px); margin: 0 12px 2px; display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: 8px; border: none; background: transparent; color: #a3a3a3; font-size: 12px; cursor: pointer; transition: background .15s, color .15s; text-align: left; font-family: inherit; overflow: hidden; }
71
+ .sidebar-project:hover { background: #1e1e1e; color: #e5e5e5; }
72
+ .sidebar-project.active { background: #1e1e1e; color: #e5e5e5; }
73
+ .sidebar-project-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
74
+
75
+ .sidebar-chat-item { width: calc(100% - 24px); margin: 0 12px 1px; display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 8px; border: none; background: transparent; color: #777; font-size: 12px; cursor: pointer; transition: background .15s, color .15s; text-align: left; font-family: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
76
+ .sidebar-chat-item:hover { background: #1a1a1a; color: #ccc; }
77
+
78
+ .sidebar-history { flex: 1; overflow-y: auto; padding-bottom: 8px; }
79
+
80
+ .sidebar-bottom { padding: 8px 12px 12px; border-top: 1px solid #1e1e1e; flex-shrink: 0; }
81
+
82
+ /* ── MAIN ── */
83
+ #main { flex: 1; display: flex; flex-direction: column; min-width: 0; height: 100vh; background: #0a0a0a; }
84
+
85
+ /* ── HEADER ── */
86
+ .chat-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid #1e1e1e; background: #0a0a0a; flex-shrink: 0; gap: 8px; }
87
+ .chat-header-left { display: flex; align-items: center; gap: 8px; }
88
+ .sidebar-toggle { background: transparent; border: none; color: #555; cursor: pointer; padding: 4px; border-radius: 6px; display: flex; transition: color .15s, background .15s; }
89
+ .sidebar-toggle:hover { color: #ccc; background: #1e1e1e; }
90
+ .chat-header-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
91
+ .header-status { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #555; }
92
+ .dot { width: 6px; height: 6px; border-radius: 50%; background: #ef4444; flex-shrink: 0; }
93
+ .dot.connected { background: #22c55e; box-shadow: 0 0 5px #22c55e; animation: pulse-g 2s infinite; }
94
+ @keyframes pulse-g { 0%,100%{opacity:1} 50%{opacity:.6} }
95
+ .hdr-btn { background: transparent; border: 1px solid #2a2a2a; color: #777; padding: 5px 10px; border-radius: 7px; font-size: 11px; cursor: pointer; font-family: inherit; transition: background .15s, color .15s, border-color .15s; white-space: nowrap; }
96
+ .hdr-btn:hover { background: #1e1e1e; color: #ccc; border-color: #3a3a3a; }
97
+ .hdr-btn.active { background: #2a1a1a; border-color: #5a2a2a; color: #f87171; }
98
+ .hdr-select { background: #111; border: 1px solid #2a2a2a; color: #ccc; padding: 5px 8px; border-radius: 7px; font-size: 11px; cursor: pointer; font-family: inherit; outline: none; }
99
+
100
+ /* ── MESSAGES ── */
101
+ #messages { flex: 1; overflow-y: auto; padding: 24px 20px; display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth; min-height: 0; }
102
+ .msg-row { display: flex; gap: 10px; max-width: 720px; animation: fadeIn .2s ease; }
103
+ @keyframes fadeIn { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:none} }
63
104
  .msg-row.user { flex-direction: row-reverse; align-self: flex-end; }
64
105
  .msg-row.assistant { align-self: flex-start; }
65
- .msg-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; border: 1px solid var(--border); background: var(--bg3); }
66
- .msg-bubble { max-width: min(70vw, 560px); padding: 12px 16px; border-radius: 16px; font-size: 14px; line-height: 1.6; }
67
- .msg-row.user .msg-bubble { background: var(--user-bg); color: #fff; border-bottom-right-radius: 4px; }
68
- .msg-row.assistant .msg-bubble { background: var(--asst-bg); border: 1px solid var(--border); border-bottom-left-radius: 4px; color: var(--text); }
69
- .msg-bubble .prose h1,.msg-bubble .prose h2,.msg-bubble .prose h3 { color: var(--accent); margin: 8px 0 4px; font-size: 1em; font-weight: 600; }
106
+ .msg-avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; background: #1a1a1a; border: 1px solid #2a2a2a; margin-top: 2px; }
107
+ .msg-bubble { max-width: min(65vw, 560px); padding: 11px 15px; border-radius: 16px; font-size: 13.5px; line-height: 1.65; }
108
+ .msg-row.user .msg-bubble { background: #c0392b; color: #fff; border-bottom-right-radius: 4px; }
109
+ .msg-row.assistant .msg-bubble { background: #141414; border: 1px solid #222; border-bottom-left-radius: 4px; color: #ddd; }
110
+ .msg-bubble .prose h1,.msg-bubble .prose h2,.msg-bubble .prose h3 { color: #f87171; margin: 8px 0 4px; font-weight: 600; }
70
111
  .msg-bubble .prose p { margin: 4px 0; }
71
- .msg-bubble .prose pre { background: var(--bg); border: 1px solid var(--border); padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 8px 0; font-size: 12px; }
72
- .msg-bubble .prose code { background: var(--bg); padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: var(--accent); }
73
- .msg-bubble .prose pre code { background: none; padding: 0; color: var(--text); }
112
+ .msg-bubble .prose pre { background: #0d0d0d; border: 1px solid #222; padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 8px 0; font-size: 12px; }
113
+ .msg-bubble .prose code { background: #0d0d0d; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: #f87171; }
114
+ .msg-bubble .prose pre code { background: none; padding: 0; color: #ccc; }
74
115
  .msg-bubble .prose ul,.msg-bubble .prose ol { padding-left: 18px; margin: 4px 0; }
75
- .msg-bubble .prose a { color: var(--accent); text-decoration: underline; }
76
- .msg-time { font-size: 10px; color: var(--text2); margin-top: 4px; text-align: right; }
77
- .msg-row.assistant .msg-time { text-align: left; }
116
+ .msg-bubble .prose a { color: #60a5fa; text-decoration: underline; }
117
+ .msg-time { font-size: 10px; color: #444; margin-top: 4px; }
118
+ .msg-row.user .msg-time { text-align: right; }
78
119
 
79
- /* ── TYPING ─── */
80
- .typing-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); animation: blink 1.3s infinite; }
81
- .typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}
120
+ /* ── TYPING ── */
121
+ .typing-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #f87171; animation: blink 1.2s infinite; }
122
+ .typing-dot:nth-child(2){animation-delay:.18s}.typing-dot:nth-child(3){animation-delay:.36s}
82
123
  @keyframes blink{0%,80%,100%{opacity:.2}40%{opacity:1}}
83
124
 
84
- /* ── INPUT ─── */
85
- .input-wrap { padding: 12px 20px 16px; border-top: 1px solid var(--border); background: var(--header-bg); backdrop-filter: blur(8px); }
86
- .input-row { display: flex; gap: 10px; align-items: flex-end; max-width: 800px; margin: 0 auto; }
87
- #input { flex: 1; background: var(--bg3); border: 1px solid var(--border); color: var(--text); border-radius: 14px; padding: 12px 16px; font-size: 14px; font-family: 'Inter', sans-serif; resize: none; min-height: 46px; max-height: 120px; outline: none; transition: border .2s, box-shadow .2s; line-height: 1.5; }
88
- #input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 15%, transparent); }
89
- #input::placeholder { color: var(--text2); }
90
- #send-btn { width: 46px; height: 46px; border-radius: 14px; background: var(--accent); border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all .2s; flex-shrink: 0; }
91
- #send-btn:hover:not(:disabled) { background: var(--accent2); transform: scale(1.05); }
92
- #send-btn:disabled { opacity: 0.4; cursor: default; transform: none; }
93
- .input-hint { font-size: 11px; color: var(--text2); text-align: center; margin-top: 6px; }
94
-
95
- /* ── WELCOME ─── */
96
- .welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 12px; text-align: center; padding: 32px; }
97
- .welcome-logo { font-family: 'JetBrains Mono', monospace; font-size: clamp(32px, 8vw, 64px); font-weight: 700; letter-spacing: 4px; background: linear-gradient(135deg, #7dd4fc, var(--accent), #0077b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1; }
98
- .welcome-sub { font-size: 14px; color: var(--text2); max-width: 380px; line-height: 1.6; }
125
+ /* ── BANNER ── */
126
+ .hc-banner { display: flex; flex-direction: column; align-items: center; padding: 24px 0 16px; flex-shrink: 0; pointer-events: none; user-select: none; opacity: 0.45; }
127
+ .hc-banner-text { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 2.4rem; letter-spacing: 0.3em; color: var(--accent); text-shadow: 0 0 32px var(--accent), 0 0 64px var(--accent-bg); line-height: 1; }
128
+ .hc-banner-sub { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--accent); letter-spacing: 0.25em; margin-top: 8px; opacity: 0.75; }
129
+
130
+ /* ── WELCOME ── */
131
+ .welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 14px; text-align: center; padding: 40px; }
132
+ .welcome-eagle { font-size: 48px; margin-bottom: 4px; filter: drop-shadow(0 0 12px rgba(239,68,68,0.3)); }
133
+ .welcome-title { font-size: 26px; font-weight: 600; color: #e5e5e5; }
134
+ .welcome-sub { font-size: 13px; color: #666; max-width: 360px; line-height: 1.6; }
99
135
  .welcome-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 4px; }
100
- .chip { background: var(--bg3); border: 1px solid var(--border); color: var(--text2); padding: 7px 14px; border-radius: 20px; font-size: 12px; cursor: pointer; transition: all .2s; }
101
- .chip:hover { border-color: var(--accent); color: var(--accent); }
102
-
103
- /* ── LOCAL TERMINAL ─── */
104
- .terminal-wrap { border-top: 1px solid var(--border); background: rgba(0,0,0,0.4); flex-shrink: 0; }
105
- .terminal-toggle { width: 100%; display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; font-size: 12px; color: var(--text2); background: transparent; border: none; cursor: pointer; transition: background .2s; }
106
- .terminal-toggle:hover { background: var(--bg3); }
107
- .terminal-body { padding: 12px 16px; display: none; }
108
- .terminal-body.open { display: block; }
109
- .terminal-btns { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
110
- .terminal-btn { padding: 6px 12px; font-size: 11px; border-radius: 6px; background: var(--bg3); border: 1px solid var(--border); color: var(--text); cursor: pointer; transition: all .2s; }
111
- .terminal-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
112
- .terminal-btn:disabled { opacity: 0.5; cursor: default; }
113
- .terminal-log { background: rgba(0,0,0,0.7); border: 1px solid var(--border); border-radius: 8px; padding: 10px; height: 140px; overflow-y: auto; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text2); white-space: pre-wrap; margin-bottom: 10px; }
114
- .terminal-log:empty::before { content: 'No commands yet. Use the buttons above or type a command below.'; color: var(--text2); opacity: 0.7; }
115
- .terminal-input-row { display: flex; gap: 8px; align-items: center; }
116
- .terminal-input { flex: 1; background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: var(--text); outline: none; }
117
- .terminal-input:focus { border-color: var(--accent); }
136
+ .chip { background: #141414; border: 1px solid #2a2a2a; color: #888; padding: 7px 14px; border-radius: 20px; font-size: 12px; cursor: pointer; transition: all .2s; font-family: inherit; }
137
+ .chip:hover { border-color: #555; color: #ccc; background: #1e1e1e; }
138
+
139
+ /* ── INPUT ── */
140
+ .input-wrap { padding: 12px 16px 14px; border-top: 1px solid #1a1a1a; background: #0a0a0a; flex-shrink: 0; }
141
+ .input-row { display: flex; gap: 8px; align-items: flex-end; max-width: 720px; margin: 0 auto; }
142
+ #input { flex: 1; background: #141414; border: 1px solid #2a2a2a; color: #e5e5e5; border-radius: 12px; padding: 11px 14px; font-size: 13.5px; font-family: 'Inter', sans-serif; resize: none; min-height: 44px; max-height: 130px; outline: none; transition: border-color .2s; line-height: 1.5; }
143
+ #input:focus { border-color: #3a3a3a; }
144
+ #input::placeholder { color: #444; }
145
+ #send-btn { width: 40px; height: 40px; border-radius: 10px; background: #c0392b; border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .2s; flex-shrink: 0; }
146
+ #send-btn:hover:not(:disabled) { background: #e74c3c; }
147
+ #send-btn:disabled { opacity: 0.3; cursor: default; }
148
+ .input-hint { font-size: 11px; color: #333; text-align: center; margin-top: 6px; max-width: 720px; margin-left: auto; margin-right: auto; }
149
+
150
+ /* ── TERMINAL PANEL ── */
151
+ #terminal-panel { border-top: 1px solid #1e1e1e; background: #0d0d0d; flex-shrink: 0; height: 220px; display: none; flex-direction: column; }
152
+ #terminal-panel.open { display: flex; }
153
+ .term-titlebar { display: flex; align-items: center; justify-content: space-between; padding: 6px 14px; background: #141414; border-bottom: 1px solid #1e1e1e; flex-shrink: 0; }
154
+ .term-dots { display: flex; gap: 5px; }
155
+ .term-dot { width: 11px; height: 11px; border-radius: 50%; }
156
+ .term-info { font-size: 11px; color: #444; font-family: 'JetBrains Mono', monospace; flex: 1; text-align: center; }
157
+ .term-titlebar-right { display: flex; align-items: center; gap: 10px; }
158
+ .term-clear-btn { font-size: 11px; color: #444; background: none; border: none; cursor: pointer; font-family: inherit; transition: color .15s; }
159
+ .term-clear-btn:hover { color: #888; }
160
+ .term-close-btn { background: none; border: none; color: #444; cursor: pointer; display: flex; transition: color .15s; }
161
+ .term-close-btn:hover { color: #ccc; }
162
+ .term-quick { display: flex; gap: 6px; padding: 6px 12px; border-bottom: 1px solid #191919; flex-shrink: 0; overflow-x: auto; }
163
+ .term-quick::-webkit-scrollbar { height: 0; }
164
+ .term-quick-btn { padding: 4px 10px; border-radius: 6px; background: #1a1a1a; border: 1px solid #2a2a2a; color: #777; font-size: 11px; cursor: pointer; font-family: 'JetBrains Mono', monospace; transition: all .15s; white-space: nowrap; flex-shrink: 0; }
165
+ .term-quick-btn:hover:not(:disabled) { background: #222; color: #ccc; border-color: #3a3a3a; }
166
+ .term-quick-btn:disabled { opacity: 0.4; cursor: default; }
167
+ #terminal-log { flex: 1; overflow-y: auto; padding: 8px 14px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #888; white-space: pre-wrap; line-height: 1.5; min-height: 0; }
168
+ .term-log-cmd { color: #67e8f9; }
169
+ .term-log-err { color: #f87171; }
170
+ .term-input-row { display: flex; align-items: center; gap: 8px; padding: 6px 12px 8px; border-top: 1px solid #1a1a1a; background: #141414; flex-shrink: 0; }
171
+ .term-prompt { color: #67e8f9; font-family: 'JetBrains Mono', monospace; font-size: 12px; flex-shrink: 0; }
172
+ #terminal-input { flex: 1; background: transparent; border: none; color: #e5e5e5; font-family: 'JetBrains Mono', monospace; font-size: 12px; outline: none; }
173
+ #terminal-input::placeholder { color: #333; }
174
+ .term-spinner { width: 12px; height: 12px; border: 1.5px solid #d97706; border-top-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; flex-shrink: 0; display: none; }
175
+ @keyframes spin { to { transform: rotate(360deg); } }
176
+
177
+ /* ── CUSTOMIZE MODAL ── */
178
+ #customize-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 999; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
179
+ #customize-modal.open { display: flex; }
180
+ .modal-box { background: #141414; border: 1px solid #2a2a2a; border-radius: 14px; padding: 24px; width: 400px; max-width: 90vw; }
181
+ .modal-title { font-size: 15px; font-weight: 600; color: #e5e5e5; margin-bottom: 16px; }
182
+ .modal-label { font-size: 12px; color: #777; margin-bottom: 4px; display: block; }
183
+ .modal-input { width: 100%; background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 8px; padding: 8px 12px; color: #e5e5e5; font-size: 13px; font-family: inherit; outline: none; margin-bottom: 12px; transition: border-color .15s; }
184
+ .modal-input:focus { border-color: #3a3a3a; }
185
+ .modal-textarea { min-height: 80px; resize: vertical; }
186
+ .modal-row { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
187
+ .modal-btn { padding: 7px 16px; border-radius: 8px; font-size: 13px; cursor: pointer; font-family: inherit; border: 1px solid #2a2a2a; transition: all .15s; }
188
+ .modal-btn-cancel { background: transparent; color: #777; }
189
+ .modal-btn-cancel:hover { background: #1e1e1e; color: #ccc; }
190
+ .modal-btn-save { background: #c0392b; border-color: #c0392b; color: #fff; }
191
+ .modal-btn-save:hover { background: #e74c3c; border-color: #e74c3c; }
118
192
  </style>
119
193
  </head>
120
194
  <body>
121
195
 
122
- <!-- HEADER BANNER -->
123
- <header class="banner-wrap">
124
- <span class="banner-eagle">🦅</span>
125
- <div style="display:flex;flex-direction:column;gap:1px">
126
- <div class="banner-logo">HYPERCLAW</div>
127
- <div class="banner-sub" id="agent-sub">AI Gateway Platform · Connecting…</div>
196
+ <!-- ── SIDEBAR ── -->
197
+ <aside id="sidebar">
198
+ <div class="sidebar-top">
199
+ <!-- Logo -->
200
+ <div class="sidebar-logo">
201
+ <span style="font-size:22px">🦅</span>
202
+ <span class="sidebar-logo-text">HyperClaw</span>
203
+ </div>
204
+
205
+ <!-- New chat -->
206
+ <button class="sidebar-new-chat" id="new-chat-btn">
207
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1v12M1 7h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
208
+ New chat
209
+ </button>
210
+
211
+ <!-- Search -->
212
+ <div class="sidebar-search" style="margin-top:6px">
213
+ <svg class="sidebar-search-icon" width="13" height="13" viewBox="0 0 13 13" fill="none"><circle cx="5.5" cy="5.5" r="4" stroke="currentColor" stroke-width="1.3"/><path d="M9 9l3 3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
214
+ <input type="text" id="search-input" placeholder="Search chats…" autocomplete="off">
215
+ </div>
128
216
  </div>
129
- <div style="flex:1"></div>
130
- <div class="banner-status">
131
- <span class="dot" id="ws-dot"></span>
132
- <span id="ws-label" style="color:var(--text2);font-size:13px">Offline</span>
217
+
218
+ <!-- Customize -->
219
+ <button class="sidebar-btn" id="customize-btn" style="margin: 2px 12px; width: calc(100% - 24px)">
220
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="2.5" stroke="currentColor" stroke-width="1.3"/><path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.5 2.5l1.1 1.1M10.4 10.4l1.1 1.1M2.5 11.5l1.1-1.1M10.4 3.6l1.1-1.1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
221
+ Customize
222
+ </button>
223
+
224
+ <!-- Projects section -->
225
+ <div class="sidebar-section">Projects</div>
226
+ <div id="projects-list">
227
+ <button class="sidebar-project active" data-preset="" id="proj-general">
228
+ <span class="sidebar-project-dot" style="background:#6b7280"></span>
229
+ General
230
+ </button>
231
+ <button class="sidebar-project" data-preset="ethical-hacker" id="proj-hacker">
232
+ <span class="sidebar-project-dot" style="background:#ef4444"></span>
233
+ Ethical Hacker
234
+ </button>
235
+ <button class="sidebar-project" data-preset="hyperclaw" id="proj-hcdev">
236
+ <span class="sidebar-project-dot" style="background:#f97316"></span>
237
+ HyperClaw Dev
238
+ </button>
239
+ <button class="sidebar-project" data-preset="osint" id="proj-osint">
240
+ <span class="sidebar-project-dot" style="background:#a855f7"></span>
241
+ OSINT
242
+ </button>
243
+ <button class="sidebar-project" id="new-project-btn" style="color:#555">
244
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M6 1v10M1 6h10" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
245
+ New project…
246
+ </button>
133
247
  </div>
134
- <div class="banner-actions" style="display:flex;align-items:center;gap:8px">
135
- <select id="prompt-select" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:6px 10px;font-size:12px;cursor:pointer">
136
- <option value="">General</option>
137
- <option value="ethical-hacker">Ethical Hacker</option>
138
- <option value="hyperclaw">HyperClaw Dev</option>
139
- <option value="osint">OSINT</option>
140
- </select>
141
- <button class="btn-icon" id="new-chat-btn" title="New chat">💬</button>
142
- <button class="btn-icon" id="clear-btn" title="Clear messages">🗑️</button>
143
- <button class="btn-icon" id="theme-btn" title="Toggle theme">🌙</button>
248
+
249
+ <!-- Chats history -->
250
+ <div class="sidebar-section">Chats</div>
251
+ <div class="sidebar-history" id="chat-history">
252
+ <div style="padding: 6px 22px; font-size:11px; color:#3a3a3a">No recent chats</div>
144
253
  </div>
145
- </header>
146
-
147
- <!-- MESSAGES -->
148
- <main id="messages">
149
- <div class="welcome" id="welcome">
150
- <div class="welcome-logo">HYPERCLAW</div>
151
- <p class="welcome-sub">Your personal AI assistant — running on your hardware. Ask anything or pick a suggestion:</p>
152
- <div class="welcome-chips">
153
- <span class="chip" onclick="fillInput(this)">What can you do?</span>
154
- <span class="chip" onclick="fillInput(this)">What's the weather today?</span>
155
- <span class="chip" onclick="fillInput(this)">Search the web for AI news</span>
156
- <span class="chip" onclick="fillInput(this)">Help me write a script</span>
157
- <span class="chip" onclick="fillInput(this)">Show system status</span>
254
+
255
+ <!-- Bottom: status -->
256
+ <div class="sidebar-bottom">
257
+ <div style="display:flex;align-items:center;gap:6px">
258
+ <span class="dot" id="ws-dot-side"></span>
259
+ <span id="ws-label-side" style="font-size:11px;color:#444">Offline</span>
158
260
  </div>
261
+ <div id="model-label" style="font-size:11px;color:#333;margin-top:2px;font-family:'JetBrains Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>
262
+ </div>
263
+ </aside>
264
+
265
+ <!-- ── MAIN ── -->
266
+ <div id="main">
267
+
268
+ <!-- Header -->
269
+ <header class="chat-header">
270
+ <div class="chat-header-left">
271
+ <button class="sidebar-toggle" id="sidebar-toggle" title="Toggle sidebar">
272
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="1.3" rx=".65" fill="currentColor"/><rect x="2" y="7.35" width="12" height="1.3" rx=".65" fill="currentColor"/><rect x="2" y="10.7" width="12" height="1.3" rx=".65" fill="currentColor"/></svg>
273
+ </button>
274
+ <span style="font-size:13px;color:#555;font-weight:500" id="active-project-label">General</span>
275
+ </div>
276
+ <div class="chat-header-right">
277
+ <div class="header-status">
278
+ <span class="dot" id="ws-dot"></span>
279
+ <span id="ws-label" style="font-size:12px;color:#444">Offline</span>
280
+ </div>
281
+ <button class="hdr-btn" id="clear-btn">Clear</button>
282
+ <button class="hdr-btn" id="terminal-btn">
283
+ <span style="display:inline-flex;align-items:center;gap:5px">
284
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M1.5 2.5L5 6L1.5 9.5M6.5 9.5H10.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
285
+ Terminal
286
+ </span>
287
+ </button>
288
+ </div>
289
+ </header>
290
+
291
+ <!-- Messages -->
292
+ <main id="messages">
293
+ <!-- Banner -->
294
+ <div class="hc-banner">
295
+ <div class="hc-banner-text">HYPERCLAW</div>
296
+ <div class="hc-banner-sub" id="banner-sub">HyperClaw Bot · AI Gateway v5.3.3</div>
297
+ </div>
298
+ <div class="welcome" id="welcome">
299
+ <div class="welcome-eagle">🦅</div>
300
+ <div class="welcome-title">What can I help with?</div>
301
+ <p class="welcome-sub">Your personal AI agent — running on your hardware. Pick a suggestion or type anything.</p>
302
+ <div class="welcome-chips">
303
+ <button class="chip" onclick="fillInput(this)">What can you do?</button>
304
+ <button class="chip" onclick="fillInput(this)">Search the web for AI news</button>
305
+ <button class="chip" onclick="fillInput(this)">Help me write a script</button>
306
+ <button class="chip" onclick="fillInput(this)">Show system status</button>
307
+ <button class="chip" onclick="fillInput(this)">Run a security audit</button>
308
+ </div>
309
+ </div>
310
+ </main>
311
+
312
+ <!-- Input -->
313
+ <div class="input-wrap">
314
+ <form class="input-row" id="form">
315
+ <textarea id="input" rows="1" placeholder="Ask HyperClaw anything… Enter to send, Shift+Enter for new line" autocomplete="off"></textarea>
316
+ <button type="submit" id="send-btn">
317
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 8l4-4 4 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
318
+ </button>
319
+ </form>
320
+ <div class="input-hint">🦅 HyperClaw · <span id="model-hint">—</span> · Shift+Enter for new line</div>
159
321
  </div>
160
- </main>
161
-
162
- <!-- INPUT -->
163
- <div class="input-wrap">
164
- <form class="input-row" id="form">
165
- <textarea id="input" rows="1" placeholder="Your AI assistant awaits — type a message and press Enter to send · HyperClaw" autocomplete="off"></textarea>
166
- <button type="submit" id="send-btn">↑</button>
167
- </form>
168
- <div class="input-hint">🦅 HyperClaw · <span id="model-hint">—</span></div>
169
- </div>
170
322
 
171
- <!-- LOCAL TERMINAL -->
172
- <div class="terminal-wrap">
173
- <button type="button" class="terminal-toggle" id="terminal-toggle">
174
- <span>› Local terminal</span>
175
- <span id="terminal-toggle-label">Show</span>
176
- </button>
177
- <div class="terminal-body" id="terminal-body">
178
- <div class="terminal-btns">
179
- <button type="button" class="terminal-btn" data-cmd="npm run build">Build</button>
180
- <button type="button" class="terminal-btn" data-cmd="npm install">Install</button>
181
- <button type="button" class="terminal-btn" data-cmd="npm test">Test</button>
182
- <button type="button" class="terminal-btn" data-cmd="npx hyperclaw doctor --fix">Doctor</button>
183
- <button type="button" class="terminal-btn" data-cmd="npx hyperclaw gateway status">Gateway status</button>
323
+ <!-- Terminal panel -->
324
+ <div id="terminal-panel">
325
+ <div class="term-titlebar">
326
+ <div class="term-dots">
327
+ <div class="term-dot" style="background:#ff5f57"></div>
328
+ <div class="term-dot" style="background:#febc2e"></div>
329
+ <div class="term-dot" style="background:#28c840"></div>
330
+ </div>
331
+ <span class="term-info" id="term-prompt-info">Terminal HyperClaw gateway</span>
332
+ <div class="term-titlebar-right">
333
+ <button class="term-clear-btn" id="term-clear-btn">clear</button>
334
+ <button class="term-close-btn" id="term-close-btn" title="Close terminal">
335
+ <svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1.5 1.5l10 10M11.5 1.5l-10 10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
336
+ </button>
337
+ </div>
184
338
  </div>
185
- <div class="terminal-log" id="terminal-log"></div>
186
- <div class="terminal-input-row">
187
- <input type="text" class="terminal-input" id="terminal-input" placeholder="Run a command in your HyperClaw workspace..." />
188
- <button type="button" class="terminal-btn" id="terminal-run">Run</button>
339
+ <div class="term-quick">
340
+ <button class="term-quick-btn" data-cmd="npm install">npm install</button>
341
+ <button class="term-quick-btn" data-cmd="npm run build">npm run build</button>
342
+ <button class="term-quick-btn" data-cmd="npm test">npm test</button>
343
+ <button class="term-quick-btn" data-cmd="npx hyperclaw doctor --fix">doctor --fix</button>
344
+ <button class="term-quick-btn" data-cmd="npx hyperclaw gateway status">gateway status</button>
345
+ </div>
346
+ <div id="terminal-log"><span style="color:#333">Use the quick buttons above or type a command below.</span></div>
347
+ <div class="term-input-row">
348
+ <span class="term-prompt">›</span>
349
+ <input type="text" id="terminal-input" placeholder="Run a command…">
350
+ <div class="term-spinner" id="term-spinner"></div>
351
+ </div>
352
+ </div>
353
+
354
+ </div>
355
+
356
+ <!-- ── CUSTOMIZE MODAL ── -->
357
+ <div id="customize-modal">
358
+ <div class="modal-box">
359
+ <div class="modal-title">✦ New Project / Agent</div>
360
+ <label class="modal-label">Project name</label>
361
+ <input type="text" class="modal-input" id="modal-name" placeholder="e.g. Bug Hunter">
362
+ <label class="modal-label">System prompt</label>
363
+ <textarea class="modal-input modal-textarea" id="modal-prompt" placeholder="e.g. You are a security researcher. Focus on vulnerabilities and exploits."></textarea>
364
+ <label class="modal-label">Color</label>
365
+ <input type="color" class="modal-input" id="modal-color" value="#6b7280" style="height:36px;padding:2px 6px;cursor:pointer">
366
+ <div class="modal-row">
367
+ <button class="modal-btn modal-btn-cancel" id="modal-cancel">Cancel</button>
368
+ <button class="modal-btn modal-btn-save" id="modal-save">Create project</button>
189
369
  </div>
190
370
  </div>
191
371
  </div>
@@ -196,70 +376,244 @@
196
376
  const apiUrl = `http://${location.hostname}:${port}`;
197
377
 
198
378
  let ws = null, isStreaming = false, currentBubble = null;
379
+ let activePreset = '';
380
+ let customProjects = JSON.parse(localStorage.getItem('hc_projects') || '[]');
381
+ let chatHistory = JSON.parse(localStorage.getItem('hc_history') || '[]');
382
+ let currentChatId = Date.now().toString();
383
+ let currentChatMessages = [];
384
+
199
385
  const messagesEl = document.getElementById('messages');
200
386
  const welcomeEl = document.getElementById('welcome');
201
- const wsDot = document.getElementById('ws-dot');
202
- const wsLabel = document.getElementById('ws-label');
203
- const agentSub = document.getElementById('agent-sub');
204
- const modelHint = document.getElementById('model-hint');
205
387
  const input = document.getElementById('input');
206
388
  const sendBtn = document.getElementById('send-btn');
207
389
  const form = document.getElementById('form');
208
390
 
209
- // ── Theme ────────────────────────────────────────────
210
- const themeBtn = document.getElementById('theme-btn');
211
- let dark = true;
212
- themeBtn.onclick = () => {
213
- dark = !dark;
214
- document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
215
- themeBtn.textContent = dark ? '🌙' : '☀️';
216
- };
217
-
218
- // ── New chat / Clear ─────────────────────────────────
219
- const promptSelect = document.getElementById('prompt-select');
220
391
  const PROMPT_PREFIX = {
221
392
  'ethical-hacker': 'Act as an ethical hacker / security researcher. Authorized testing only. ',
222
393
  'hyperclaw': 'You are helping with HyperClaw development. Be concise and code-focused. ',
223
394
  'osint': 'Act as an OSINT analyst. Passive reconnaissance, open-source research. '
224
395
  };
225
- document.getElementById('new-chat-btn').onclick = () => {
226
- messagesEl.querySelectorAll('.msg-row').forEach(e => e.remove());
227
- welcomeEl.style.display = 'flex';
396
+
397
+ // ── Sidebar toggle ───────────────────────────────────
398
+ document.getElementById('sidebar-toggle').onclick = () => {
399
+ document.getElementById('sidebar').classList.toggle('collapsed');
228
400
  };
229
- document.getElementById('clear-btn').onclick = () => {
401
+
402
+ // ── Project switching ────────────────────────────────
403
+ function setActiveProject(preset, name, dotColor) {
404
+ activePreset = preset;
405
+ document.querySelectorAll('.sidebar-project').forEach(b => b.classList.remove('active'));
406
+ document.getElementById('active-project-label').textContent = name || 'General';
407
+ }
408
+
409
+ document.querySelectorAll('.sidebar-project[data-preset]').forEach(btn => {
410
+ btn.onclick = () => {
411
+ const names = { '': 'General', 'ethical-hacker': 'Ethical Hacker', 'hyperclaw': 'HyperClaw Dev', 'osint': 'OSINT' };
412
+ document.querySelectorAll('.sidebar-project').forEach(b => b.classList.remove('active'));
413
+ btn.classList.add('active');
414
+ setActiveProject(btn.dataset.preset, names[btn.dataset.preset]);
415
+ };
416
+ });
417
+
418
+ // ── Chat history ─────────────────────────────────────
419
+ function saveCurrentChat(firstMsg) {
420
+ if (!firstMsg) return;
421
+ const existing = chatHistory.find(c => c.id === currentChatId);
422
+ if (existing) { existing.title = firstMsg.slice(0, 40); }
423
+ else { chatHistory.unshift({ id: currentChatId, title: firstMsg.slice(0, 40), ts: Date.now() }); }
424
+ if (chatHistory.length > 30) chatHistory = chatHistory.slice(0, 30);
425
+ localStorage.setItem('hc_history', JSON.stringify(chatHistory));
426
+ renderHistory();
427
+ }
428
+
429
+ function renderHistory(filter) {
430
+ const el = document.getElementById('chat-history');
431
+ const items = filter
432
+ ? chatHistory.filter(c => c.title.toLowerCase().includes(filter.toLowerCase()))
433
+ : chatHistory;
434
+ if (!items.length) { el.innerHTML = '<div style="padding:6px 22px;font-size:11px;color:#3a3a3a">' + (filter ? 'No results' : 'No recent chats') + '</div>'; return; }
435
+ el.innerHTML = items.map(c => `<button class="sidebar-chat-item" onclick="loadChat('${c.id}')" title="${escHtml(c.title)}">💬 ${escHtml(c.title)}</button>`).join('');
436
+ }
437
+
438
+ function loadChat(id) {
439
+ // For now just label it — full persistence would require storing messages
440
+ const chat = chatHistory.find(c => c.id === id);
441
+ if (chat) document.getElementById('active-project-label').textContent = chat.title.slice(0,20) + '…';
442
+ }
443
+
444
+ renderHistory();
445
+
446
+ // Search chats
447
+ document.getElementById('search-input').oninput = function() { renderHistory(this.value); };
448
+
449
+ // ── New chat ─────────────────────────────────────────
450
+ document.getElementById('new-chat-btn').onclick = startNewChat;
451
+ document.getElementById('clear-btn').onclick = () => { clearMessages(); };
452
+
453
+ function startNewChat() {
454
+ currentChatId = Date.now().toString();
455
+ currentChatMessages = [];
456
+ clearMessages();
457
+ }
458
+
459
+ function clearMessages() {
230
460
  messagesEl.querySelectorAll('.msg-row').forEach(e => e.remove());
231
461
  welcomeEl.style.display = 'flex';
462
+ }
463
+
464
+ // ── Customize modal ──────────────────────────────────
465
+ document.getElementById('customize-btn').onclick = () => {
466
+ document.getElementById('customize-modal').classList.add('open');
467
+ document.getElementById('modal-name').focus();
468
+ };
469
+ document.getElementById('new-project-btn').onclick = () => {
470
+ document.getElementById('customize-modal').classList.add('open');
471
+ document.getElementById('modal-name').focus();
472
+ };
473
+ document.getElementById('modal-cancel').onclick = () => document.getElementById('customize-modal').classList.remove('open');
474
+ document.getElementById('customize-modal').onclick = (e) => { if (e.target === e.currentTarget) e.currentTarget.classList.remove('open'); };
475
+
476
+ document.getElementById('modal-save').onclick = () => {
477
+ const name = document.getElementById('modal-name').value.trim();
478
+ const prompt = document.getElementById('modal-prompt').value.trim();
479
+ const color = document.getElementById('modal-color').value;
480
+ if (!name) return;
481
+ const id = 'custom-' + Date.now();
482
+ customProjects.push({ id, name, prompt, color });
483
+ localStorage.setItem('hc_projects', JSON.stringify(customProjects));
484
+ PROMPT_PREFIX[id] = prompt + ' ';
485
+ addProjectToSidebar({ id, name, color });
486
+ document.getElementById('customize-modal').classList.remove('open');
487
+ document.getElementById('modal-name').value = '';
488
+ document.getElementById('modal-prompt').value = '';
489
+ document.getElementById('modal-color').value = '#6b7280';
232
490
  };
233
491
 
234
- // ── Chips ────────────────────────────────────────────
235
- function fillInput(el) {
236
- input.value = el.textContent;
237
- input.focus();
238
- autoResize();
492
+ function addProjectToSidebar(proj) {
493
+ const btn = document.createElement('button');
494
+ btn.className = 'sidebar-project';
495
+ btn.dataset.preset = proj.id;
496
+ btn.innerHTML = `<span class="sidebar-project-dot" style="background:${proj.color}"></span>${escHtml(proj.name)}`;
497
+ btn.onclick = () => {
498
+ document.querySelectorAll('.sidebar-project').forEach(b => b.classList.remove('active'));
499
+ btn.classList.add('active');
500
+ setActiveProject(proj.id, proj.name);
501
+ };
502
+ document.getElementById('new-project-btn').before(btn);
239
503
  }
240
504
 
505
+ // Restore custom projects
506
+ customProjects.forEach(proj => {
507
+ PROMPT_PREFIX[proj.id] = proj.prompt + ' ';
508
+ addProjectToSidebar(proj);
509
+ });
510
+
511
+ // ── Terminal ─────────────────────────────────────────
512
+ const terminalPanel = document.getElementById('terminal-panel');
513
+ const terminalBtn = document.getElementById('terminal-btn');
514
+ const terminalLog = document.getElementById('terminal-log');
515
+ const terminalInput = document.getElementById('terminal-input');
516
+ const termSpinner = document.getElementById('term-spinner');
517
+ let termRunning = false;
518
+
519
+ terminalBtn.onclick = toggleTerminal;
520
+ document.getElementById('term-close-btn').onclick = () => {
521
+ terminalPanel.classList.remove('open');
522
+ terminalBtn.classList.remove('active');
523
+ };
524
+ document.getElementById('term-clear-btn').onclick = () => {
525
+ terminalLog.innerHTML = '<span style="color:#333">Terminal cleared.</span>';
526
+ };
527
+
528
+ function toggleTerminal() {
529
+ const isOpen = terminalPanel.classList.toggle('open');
530
+ terminalBtn.classList.toggle('active', isOpen);
531
+ if (isOpen) setTimeout(() => terminalInput.focus(), 50);
532
+ }
533
+
534
+ async function runTerminalCmd(cmd) {
535
+ cmd = (cmd || '').trim();
536
+ if (!cmd || termRunning) return;
537
+ termRunning = true;
538
+ termSpinner.style.display = 'block';
539
+ document.querySelectorAll('.term-quick-btn').forEach(b => b.disabled = true);
540
+
541
+ const cmdLine = document.createElement('div');
542
+ cmdLine.className = 'term-log-cmd';
543
+ cmdLine.textContent = '$ ' + cmd;
544
+ if (terminalLog.querySelector('span')) terminalLog.innerHTML = '';
545
+ terminalLog.appendChild(cmdLine);
546
+ terminalLog.scrollTop = terminalLog.scrollHeight;
547
+
548
+ try {
549
+ const res = await fetch(apiUrl + '/api/terminal', {
550
+ method: 'POST', headers: {'Content-Type': 'application/json'},
551
+ body: JSON.stringify({command: cmd})
552
+ });
553
+ const data = await res.json();
554
+ if (data.user || data.cwd) {
555
+ document.getElementById('term-prompt-info').textContent = `${data.user || 'user'}@${data.hostname || 'local'}:${data.cwd || ''}`;
556
+ }
557
+ if (data.stdout) {
558
+ const out = document.createElement('div');
559
+ out.textContent = data.stdout.trimEnd();
560
+ terminalLog.appendChild(out);
561
+ }
562
+ if (data.stderr) {
563
+ const err = document.createElement('div');
564
+ err.className = 'term-log-err';
565
+ err.textContent = data.stderr.trimEnd();
566
+ terminalLog.appendChild(err);
567
+ }
568
+ if (!data.stdout && !data.stderr) {
569
+ const ex = document.createElement('div');
570
+ ex.style.color = '#444';
571
+ ex.textContent = '(exit ' + (data.code ?? 0) + ')';
572
+ terminalLog.appendChild(ex);
573
+ }
574
+ } catch (e) {
575
+ const err = document.createElement('div');
576
+ err.className = 'term-log-err';
577
+ err.textContent = 'Error: ' + (e.message || String(e));
578
+ terminalLog.appendChild(err);
579
+ } finally {
580
+ termRunning = false;
581
+ termSpinner.style.display = 'none';
582
+ document.querySelectorAll('.term-quick-btn').forEach(b => b.disabled = false);
583
+ terminalLog.scrollTop = terminalLog.scrollHeight;
584
+ terminalInput.focus();
585
+ }
586
+ }
587
+
588
+ document.querySelectorAll('.term-quick-btn').forEach(btn => {
589
+ btn.onclick = () => runTerminalCmd(btn.dataset.cmd);
590
+ });
591
+ terminalInput.onkeydown = (e) => {
592
+ if (e.key === 'Enter') { runTerminalCmd(terminalInput.value); terminalInput.value = ''; }
593
+ };
594
+
241
595
  // ── WebSocket ────────────────────────────────────────
242
596
  function setConnected(ok) {
243
- wsDot.className = 'dot' + (ok ? ' connected' : '');
244
- wsLabel.textContent = ok ? 'Connected' : 'Reconnecting…';
245
- wsLabel.style.color = ok ? '#22c55e' : 'var(--text2)';
597
+ [document.getElementById('ws-dot'), document.getElementById('ws-dot-side')].forEach(d => {
598
+ if (d) d.className = 'dot' + (ok ? ' connected' : '');
599
+ });
600
+ const label = ok ? 'Connected' : 'Reconnecting…';
601
+ const color = ok ? '#22c55e' : '#444';
602
+ [document.getElementById('ws-label'), document.getElementById('ws-label-side')].forEach(el => {
603
+ if (el) { el.textContent = label; el.style.color = color; }
604
+ });
246
605
  }
247
606
 
248
607
  function connectWs() {
249
608
  ws = new WebSocket(wsUrl);
250
- ws.onopen = () => { setConnected(true); };
609
+ ws.onopen = () => setConnected(true);
251
610
  ws.onclose = () => { setConnected(false); setTimeout(connectWs, 3000); };
252
611
  ws.onerror = () => setConnected(false);
253
612
  ws.onmessage = (e) => {
254
613
  try {
255
614
  const msg = JSON.parse(e.data);
256
- if (msg.type === 'connect.ok' || msg.type === 'auth.ok') {
257
- // connected
258
- } else if (msg.type === 'chat:chunk') {
259
- if (!currentBubble) {
260
- removeTyping();
261
- currentBubble = addMsg('assistant', '', true);
262
- }
615
+ if (msg.type === 'chat:chunk') {
616
+ if (!currentBubble) { removeTyping(); currentBubble = addMsg('assistant', '', true); }
263
617
  currentBubble._raw = (currentBubble._raw || '') + (msg.content || '');
264
618
  currentBubble.textContent = currentBubble._raw;
265
619
  messagesEl.scrollTop = messagesEl.scrollHeight;
@@ -290,11 +644,8 @@
290
644
  messagesEl.scrollTop = messagesEl.scrollHeight;
291
645
  }
292
646
 
293
- // ── Message helpers ───────────────────────────────────
294
- function now() {
295
- return new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
296
- }
297
- let lastTimestampEl = null;
647
+ // ── Messages ─────────────────────────────────────────
648
+ function now() { return new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); }
298
649
 
299
650
  function addMsg(role, content, streaming = false) {
300
651
  welcomeEl.style.display = 'none';
@@ -318,11 +669,13 @@
318
669
  }
319
670
 
320
671
  function addTimestamp() {
321
- const t = document.createElement('div');
322
- t.className = 'msg-time';
323
- t.textContent = now();
324
672
  const last = messagesEl.querySelector('.msg-row.assistant:last-child');
325
- if (last) last.appendChild(t);
673
+ if (last) {
674
+ const t = document.createElement('div');
675
+ t.className = 'msg-time';
676
+ t.textContent = now();
677
+ last.appendChild(t);
678
+ }
326
679
  }
327
680
 
328
681
  let workingTicker = null;
@@ -332,12 +685,10 @@
332
685
  row.className = 'msg-row assistant';
333
686
  row.id = 'typing-row';
334
687
  let sec = 0;
335
- row.innerHTML = '<div class="msg-avatar">🦅</div><div class="msg-bubble" style="padding:14px 18px"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span> <span id="working-text" style="margin-left:8px;color:var(--text2);font-size:12px">Working (0s)</span></div>';
688
+ row.innerHTML = '<div class="msg-avatar">🦅</div><div class="msg-bubble" style="padding:12px 16px"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span><span id="wt" style="margin-left:8px;color:#444;font-size:11px">Thinking…</span></div>';
336
689
  messagesEl.appendChild(row);
337
690
  messagesEl.scrollTop = messagesEl.scrollHeight;
338
- workingTicker = setInterval(() => {
339
- sec++; const el = document.getElementById('working-text'); if (el) el.textContent = 'Working (' + sec + 's)';
340
- }, 1000);
691
+ workingTicker = setInterval(() => { sec++; const el = document.getElementById('wt'); if (el) el.textContent = sec + 's'; }, 1000);
341
692
  }
342
693
 
343
694
  function removeTyping() {
@@ -345,16 +696,18 @@
345
696
  document.getElementById('typing-row')?.remove();
346
697
  }
347
698
 
348
- function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
699
+ function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
349
700
 
350
701
  // ── Send ─────────────────────────────────────────────
702
+ let msgCount = 0;
351
703
  form.onsubmit = async (e) => {
352
704
  e.preventDefault();
353
705
  const msg = input.value.trim();
354
706
  if (!msg || isStreaming) return;
355
- const preset = promptSelect?.value || '';
356
- const prefix = PROMPT_PREFIX[preset] || '';
707
+ const prefix = PROMPT_PREFIX[activePreset] || '';
357
708
  const fullMsg = prefix ? prefix + msg : msg;
709
+ if (msgCount === 0) saveCurrentChat(msg);
710
+ msgCount++;
358
711
  addMsg('user', msg);
359
712
  input.value = '';
360
713
  input.style.height = 'auto';
@@ -365,7 +718,6 @@
365
718
  if (ws && ws.readyState === WebSocket.OPEN) {
366
719
  ws.send(JSON.stringify({ type: 'chat:message', content: fullMsg, source: 'webchat' }));
367
720
  } else {
368
- // REST fallback
369
721
  try {
370
722
  const res = await fetch(apiUrl + '/api/chat', {
371
723
  method: 'POST', headers: {'Content-Type':'application/json'},
@@ -383,70 +735,29 @@
383
735
  }
384
736
  };
385
737
 
738
+ // ── Chips ────────────────────────────────────────────
739
+ function fillInput(el) { input.value = el.textContent; input.focus(); autoResize(); }
740
+
386
741
  // ── Textarea auto-resize ──────────────────────────────
387
742
  function autoResize() {
388
743
  input.style.height = 'auto';
389
- input.style.height = Math.min(input.scrollHeight, 120) + 'px';
744
+ input.style.height = Math.min(input.scrollHeight, 130) + 'px';
390
745
  }
391
746
  input.addEventListener('input', autoResize);
392
747
  input.addEventListener('keydown', (e) => {
393
748
  if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); form.dispatchEvent(new Event('submit')); }
394
749
  });
395
750
 
396
- // ── Local Terminal ───────────────────────────────────
397
- const terminalToggle = document.getElementById('terminal-toggle');
398
- const terminalBody = document.getElementById('terminal-body');
399
- const terminalLog = document.getElementById('terminal-log');
400
- const terminalInput = document.getElementById('terminal-input');
401
- const terminalRun = document.getElementById('terminal-run');
402
- const toggleLabel = document.getElementById('terminal-toggle-label');
403
- let terminalRunning = false;
404
-
405
- terminalToggle.onclick = () => {
406
- terminalBody.classList.toggle('open', !terminalBody.classList.contains('open'));
407
- toggleLabel.textContent = terminalBody.classList.contains('open') ? 'Hide' : 'Show';
408
- };
409
-
410
- async function runTerminalCmd(cmd) {
411
- cmd = (cmd || '').trim();
412
- if (!cmd || terminalRunning) return;
413
- terminalLog.append('$ ' + cmd + '\n');
414
- terminalLog.scrollTop = terminalLog.scrollHeight;
415
- terminalRunning = true;
416
- terminalRun.disabled = true;
417
- document.querySelectorAll('.terminal-btn[data-cmd]').forEach(b => b.disabled = true);
418
- try {
419
- const res = await fetch(apiUrl + '/api/terminal', {
420
- method: 'POST', headers: {'Content-Type': 'application/json'},
421
- body: JSON.stringify({command: cmd})
422
- });
423
- const data = await res.json();
424
- if (data.stdout) terminalLog.append(data.stdout.trimEnd() + '\n');
425
- if (data.stderr) terminalLog.append('! ' + data.stderr.trimEnd() + '\n');
426
- terminalLog.append('(exit code ' + (data.code ?? 0) + ')\n');
427
- } catch (e) {
428
- terminalLog.append('Error: ' + (e.message || String(e)) + '\n');
429
- } finally {
430
- terminalRunning = false;
431
- terminalRun.disabled = false;
432
- document.querySelectorAll('.terminal-btn[data-cmd]').forEach(b => b.disabled = false);
433
- terminalLog.scrollTop = terminalLog.scrollHeight;
434
- }
435
- }
436
-
437
- document.querySelectorAll('.terminal-btn[data-cmd]').forEach(btn => {
438
- btn.onclick = () => runTerminalCmd(btn.dataset.cmd);
439
- });
440
- terminalRun.onclick = () => { runTerminalCmd(terminalInput.value); terminalInput.value = ''; };
441
- terminalInput.onkeydown = (e) => {
442
- if (e.key === 'Enter') { runTerminalCmd(terminalInput.value); terminalInput.value = ''; }
443
- };
444
-
445
751
  // ── Init ─────────────────────────────────────────────
446
752
  connectWs();
447
753
  fetch(apiUrl + '/api/status').then(r => r.json()).then(d => {
448
- if (d.agentName) agentSub.textContent = `${d.agentName} · ${d.model || '—'} · port ${d.port || port}`;
449
- if (d.model) modelHint.textContent = d.model;
754
+ if (d.model) {
755
+ document.getElementById('model-hint').textContent = d.model;
756
+ document.getElementById('model-label').textContent = d.model;
757
+ const ver = d.version || '5.3.3';
758
+ document.getElementById('banner-sub').textContent = `HyperClaw Bot \u00b7 AI Gateway v${ver}`;
759
+ }
760
+ document.documentElement.setAttribute('data-daemon', d.daemonMode ? 'true' : 'false');
450
761
  }).catch(() => {});
451
762
  </script>
452
763
  </body>