@simonyea/holysheep-cli 1.7.135 → 2.0.0
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/README.md +2 -1
- package/package.json +2 -2
- package/src/tools/droid.js +4 -2
- package/src/utils/which.js +28 -2
- package/src/webui/index.html +1329 -584
- package/src/webui/server.js +269 -32
- package/src/webui/workspace-runtime.js +288 -0
- package/src/webui/workspace-store.js +325 -0
- package/tests/workspace-store.test.js +57 -0
package/src/webui/index.html
CHANGED
|
@@ -3,700 +3,1445 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>HolySheep
|
|
6
|
+
<title>HolySheep Workspace</title>
|
|
7
7
|
<style>
|
|
8
|
-
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
9
|
:root {
|
|
10
|
-
--bg: #
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
10
|
+
--bg: #f4f4ef;
|
|
11
|
+
--panel: #fcfbf8;
|
|
12
|
+
--panel-2: #f6f1e9;
|
|
13
|
+
--panel-3: #ece4d8;
|
|
14
|
+
--line: rgba(59, 44, 29, 0.12);
|
|
15
|
+
--text: #231c14;
|
|
16
|
+
--muted: #7a6855;
|
|
17
|
+
--accent: #d06e2b;
|
|
18
|
+
--accent-2: #e89c66;
|
|
19
|
+
--success: #2e8b57;
|
|
20
|
+
--error: #b2412d;
|
|
21
|
+
--warning: #b57f20;
|
|
22
|
+
--shadow: 0 20px 50px rgba(73, 48, 20, 0.08);
|
|
23
|
+
--radius-xl: 22px;
|
|
24
|
+
--radius-lg: 16px;
|
|
25
|
+
--radius-md: 12px;
|
|
26
|
+
--font-ui: "SF Pro Display", "Segoe UI", sans-serif;
|
|
27
|
+
--font-mono: "SF Mono", "Cascadia Code", monospace;
|
|
19
28
|
}
|
|
20
29
|
@media (prefers-color-scheme: dark) {
|
|
21
30
|
:root {
|
|
22
|
-
--bg: #
|
|
23
|
-
--
|
|
24
|
-
--
|
|
31
|
+
--bg: #13110f;
|
|
32
|
+
--panel: #1a1714;
|
|
33
|
+
--panel-2: #211d19;
|
|
34
|
+
--panel-3: #2a241e;
|
|
35
|
+
--line: rgba(255, 245, 231, 0.08);
|
|
36
|
+
--text: #f6efe6;
|
|
37
|
+
--muted: #b49d87;
|
|
38
|
+
--accent: #ef8d45;
|
|
39
|
+
--accent-2: #f1b281;
|
|
40
|
+
--success: #5bc48d;
|
|
41
|
+
--error: #eb7c67;
|
|
42
|
+
--warning: #f4bf5a;
|
|
43
|
+
--shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
|
|
25
44
|
}
|
|
26
45
|
}
|
|
27
|
-
html { font-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.
|
|
46
|
+
html, body { margin: 0; min-height: 100%; background: radial-gradient(circle at top left, rgba(208,110,43,0.10), transparent 30%), var(--bg); color: var(--text); font-family: var(--font-ui); }
|
|
47
|
+
button, input, textarea, select { font: inherit; }
|
|
48
|
+
button { cursor: pointer; }
|
|
49
|
+
.app-shell {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: 270px 1fr;
|
|
52
|
+
min-height: 100vh;
|
|
53
|
+
}
|
|
54
|
+
.sidebar {
|
|
55
|
+
padding: 28px 18px;
|
|
56
|
+
border-right: 1px solid var(--line);
|
|
57
|
+
background: linear-gradient(180deg, rgba(208,110,43,0.08), transparent 18%), var(--panel);
|
|
58
|
+
position: sticky;
|
|
59
|
+
top: 0;
|
|
60
|
+
height: 100vh;
|
|
61
|
+
overflow: auto;
|
|
62
|
+
}
|
|
63
|
+
.brand {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: space-between;
|
|
67
|
+
margin-bottom: 24px;
|
|
68
|
+
}
|
|
69
|
+
.brand-title {
|
|
70
|
+
font-size: 22px;
|
|
71
|
+
font-weight: 800;
|
|
72
|
+
letter-spacing: -0.04em;
|
|
73
|
+
}
|
|
74
|
+
.brand-title span { color: var(--accent); }
|
|
75
|
+
.brand-sub {
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
margin-top: 6px;
|
|
79
|
+
}
|
|
80
|
+
.nav-group {
|
|
81
|
+
margin-bottom: 24px;
|
|
82
|
+
}
|
|
83
|
+
.nav-label {
|
|
84
|
+
font-size: 11px;
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
letter-spacing: 0.14em;
|
|
88
|
+
margin-bottom: 10px;
|
|
89
|
+
}
|
|
90
|
+
.nav-item {
|
|
91
|
+
width: 100%;
|
|
92
|
+
text-align: left;
|
|
93
|
+
border: 1px solid transparent;
|
|
94
|
+
background: transparent;
|
|
95
|
+
color: var(--text);
|
|
96
|
+
padding: 12px 14px;
|
|
97
|
+
border-radius: 14px;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 10px;
|
|
101
|
+
margin-bottom: 6px;
|
|
102
|
+
}
|
|
103
|
+
.nav-item.active {
|
|
104
|
+
background: var(--panel-2);
|
|
105
|
+
border-color: var(--line);
|
|
106
|
+
box-shadow: var(--shadow);
|
|
107
|
+
}
|
|
108
|
+
.nav-dot {
|
|
109
|
+
width: 8px;
|
|
110
|
+
height: 8px;
|
|
111
|
+
border-radius: 999px;
|
|
112
|
+
background: var(--accent);
|
|
113
|
+
flex: none;
|
|
114
|
+
}
|
|
115
|
+
.sidebar-card {
|
|
116
|
+
background: var(--panel-2);
|
|
117
|
+
border: 1px solid var(--line);
|
|
118
|
+
border-radius: var(--radius-lg);
|
|
119
|
+
padding: 16px;
|
|
120
|
+
}
|
|
121
|
+
.sidebar-card h4 {
|
|
122
|
+
margin: 0 0 8px;
|
|
123
|
+
font-size: 14px;
|
|
124
|
+
}
|
|
125
|
+
.sidebar-card p {
|
|
126
|
+
margin: 0;
|
|
127
|
+
color: var(--muted);
|
|
128
|
+
font-size: 12px;
|
|
129
|
+
line-height: 1.6;
|
|
130
|
+
}
|
|
131
|
+
.main {
|
|
132
|
+
padding: 28px 28px 42px;
|
|
133
|
+
min-width: 0;
|
|
134
|
+
}
|
|
135
|
+
.topbar {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
gap: 16px;
|
|
140
|
+
margin-bottom: 22px;
|
|
141
|
+
}
|
|
142
|
+
.topbar-title h1 {
|
|
143
|
+
margin: 0;
|
|
144
|
+
font-size: 28px;
|
|
145
|
+
letter-spacing: -0.05em;
|
|
146
|
+
}
|
|
147
|
+
.topbar-title p {
|
|
148
|
+
margin: 8px 0 0;
|
|
149
|
+
color: var(--muted);
|
|
150
|
+
font-size: 13px;
|
|
151
|
+
}
|
|
152
|
+
.topbar-actions {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 12px;
|
|
156
|
+
}
|
|
157
|
+
.pill {
|
|
158
|
+
border-radius: 999px;
|
|
159
|
+
padding: 9px 14px;
|
|
160
|
+
background: var(--panel);
|
|
161
|
+
border: 1px solid var(--line);
|
|
162
|
+
color: var(--muted);
|
|
163
|
+
font-size: 12px;
|
|
164
|
+
}
|
|
165
|
+
.btn {
|
|
166
|
+
border-radius: 12px;
|
|
167
|
+
border: 1px solid var(--line);
|
|
168
|
+
background: var(--panel);
|
|
169
|
+
color: var(--text);
|
|
170
|
+
padding: 10px 14px;
|
|
171
|
+
transition: 0.15s ease;
|
|
172
|
+
}
|
|
173
|
+
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
174
|
+
.btn.primary {
|
|
175
|
+
background: var(--accent);
|
|
176
|
+
border-color: var(--accent);
|
|
177
|
+
color: #fff;
|
|
178
|
+
font-weight: 700;
|
|
179
|
+
}
|
|
180
|
+
.btn.primary:hover { filter: brightness(1.06); color: #fff; }
|
|
181
|
+
.btn.warn { color: var(--warning); }
|
|
182
|
+
.btn.danger { color: var(--error); }
|
|
183
|
+
.grid {
|
|
184
|
+
display: grid;
|
|
185
|
+
gap: 18px;
|
|
186
|
+
}
|
|
187
|
+
.cards-3 {
|
|
188
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
189
|
+
}
|
|
190
|
+
.panel {
|
|
191
|
+
background: var(--panel);
|
|
192
|
+
border: 1px solid var(--line);
|
|
193
|
+
border-radius: var(--radius-xl);
|
|
194
|
+
box-shadow: var(--shadow);
|
|
195
|
+
}
|
|
196
|
+
.panel-header {
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: space-between;
|
|
200
|
+
gap: 16px;
|
|
201
|
+
padding: 18px 20px 0;
|
|
202
|
+
}
|
|
203
|
+
.panel-header h2, .panel-header h3 {
|
|
204
|
+
margin: 0;
|
|
205
|
+
font-size: 18px;
|
|
206
|
+
letter-spacing: -0.04em;
|
|
207
|
+
}
|
|
208
|
+
.panel-header p {
|
|
209
|
+
margin: 8px 0 0;
|
|
210
|
+
color: var(--muted);
|
|
211
|
+
font-size: 13px;
|
|
212
|
+
}
|
|
213
|
+
.panel-body { padding: 18px 20px 20px; }
|
|
214
|
+
.metric {
|
|
215
|
+
padding: 20px;
|
|
216
|
+
}
|
|
217
|
+
.metric-value {
|
|
218
|
+
font-size: 30px;
|
|
219
|
+
font-weight: 800;
|
|
220
|
+
letter-spacing: -0.05em;
|
|
221
|
+
}
|
|
222
|
+
.metric-label {
|
|
223
|
+
margin-top: 8px;
|
|
224
|
+
color: var(--muted);
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
}
|
|
227
|
+
.workspace-shell {
|
|
228
|
+
display: grid;
|
|
229
|
+
grid-template-columns: 340px 1fr;
|
|
230
|
+
gap: 18px;
|
|
231
|
+
min-height: calc(100vh - 170px);
|
|
232
|
+
}
|
|
233
|
+
.conversation-rail {
|
|
234
|
+
display: grid;
|
|
235
|
+
grid-template-rows: auto auto 1fr;
|
|
236
|
+
gap: 14px;
|
|
237
|
+
}
|
|
238
|
+
.search-box {
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: 10px;
|
|
242
|
+
padding: 12px 14px;
|
|
243
|
+
background: var(--panel-2);
|
|
244
|
+
border-radius: 14px;
|
|
245
|
+
border: 1px solid var(--line);
|
|
246
|
+
}
|
|
247
|
+
.search-box input {
|
|
248
|
+
border: 0;
|
|
249
|
+
outline: none;
|
|
250
|
+
width: 100%;
|
|
251
|
+
background: transparent;
|
|
252
|
+
color: var(--text);
|
|
253
|
+
}
|
|
254
|
+
.conversation-list {
|
|
255
|
+
padding: 6px;
|
|
256
|
+
max-height: calc(100vh - 340px);
|
|
257
|
+
overflow: auto;
|
|
258
|
+
}
|
|
259
|
+
.conversation-item {
|
|
260
|
+
width: 100%;
|
|
261
|
+
text-align: left;
|
|
262
|
+
background: transparent;
|
|
263
|
+
border: 1px solid transparent;
|
|
264
|
+
border-radius: 16px;
|
|
265
|
+
padding: 14px;
|
|
266
|
+
margin-bottom: 8px;
|
|
267
|
+
}
|
|
268
|
+
.conversation-item.active {
|
|
269
|
+
background: var(--panel-2);
|
|
270
|
+
border-color: var(--line);
|
|
271
|
+
}
|
|
272
|
+
.conversation-title {
|
|
273
|
+
font-size: 15px;
|
|
274
|
+
font-weight: 700;
|
|
275
|
+
}
|
|
276
|
+
.conversation-meta {
|
|
277
|
+
margin-top: 8px;
|
|
278
|
+
font-size: 12px;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
line-height: 1.5;
|
|
281
|
+
}
|
|
282
|
+
.search-result-block {
|
|
283
|
+
padding: 12px 14px;
|
|
284
|
+
border-radius: 14px;
|
|
285
|
+
background: var(--panel-2);
|
|
286
|
+
border: 1px solid var(--line);
|
|
287
|
+
}
|
|
288
|
+
.search-result-title {
|
|
289
|
+
font-size: 12px;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
margin-bottom: 8px;
|
|
292
|
+
}
|
|
293
|
+
.search-result-item {
|
|
294
|
+
display: block;
|
|
295
|
+
width: 100%;
|
|
296
|
+
text-align: left;
|
|
297
|
+
border: 0;
|
|
298
|
+
background: transparent;
|
|
299
|
+
padding: 8px 0;
|
|
300
|
+
color: var(--text);
|
|
301
|
+
border-bottom: 1px dashed var(--line);
|
|
302
|
+
}
|
|
303
|
+
.search-result-item:last-child { border-bottom: 0; }
|
|
304
|
+
.conversation-stage {
|
|
305
|
+
display: grid;
|
|
306
|
+
grid-template-rows: auto 1fr auto;
|
|
307
|
+
}
|
|
308
|
+
.stage-toolbar {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
justify-content: space-between;
|
|
312
|
+
gap: 12px;
|
|
313
|
+
padding: 18px 20px;
|
|
314
|
+
border-bottom: 1px solid var(--line);
|
|
315
|
+
}
|
|
316
|
+
.stage-toolbar-left {
|
|
317
|
+
display: flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
gap: 10px;
|
|
320
|
+
flex-wrap: wrap;
|
|
321
|
+
}
|
|
322
|
+
.stage-toolbar h3 { margin: 0; font-size: 20px; }
|
|
323
|
+
.badge {
|
|
324
|
+
display: inline-flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
gap: 6px;
|
|
327
|
+
border-radius: 999px;
|
|
328
|
+
padding: 6px 10px;
|
|
329
|
+
font-size: 12px;
|
|
330
|
+
border: 1px solid var(--line);
|
|
331
|
+
color: var(--muted);
|
|
332
|
+
background: var(--panel-2);
|
|
333
|
+
}
|
|
334
|
+
.badge.ok { color: var(--success); }
|
|
335
|
+
.badge.error { color: var(--error); }
|
|
336
|
+
.badge.warn { color: var(--warning); }
|
|
337
|
+
.message-list {
|
|
338
|
+
padding: 18px 20px;
|
|
339
|
+
overflow: auto;
|
|
340
|
+
min-height: 420px;
|
|
341
|
+
}
|
|
342
|
+
.message {
|
|
343
|
+
max-width: 82%;
|
|
344
|
+
padding: 14px 16px;
|
|
345
|
+
border-radius: 18px;
|
|
346
|
+
margin-bottom: 14px;
|
|
347
|
+
line-height: 1.7;
|
|
348
|
+
white-space: pre-wrap;
|
|
349
|
+
word-break: break-word;
|
|
350
|
+
}
|
|
351
|
+
.message.user {
|
|
352
|
+
margin-left: auto;
|
|
353
|
+
background: linear-gradient(180deg, rgba(208,110,43,0.12), rgba(208,110,43,0.18));
|
|
354
|
+
border: 1px solid rgba(208,110,43,0.18);
|
|
355
|
+
}
|
|
356
|
+
.message.assistant {
|
|
357
|
+
background: var(--panel-2);
|
|
358
|
+
border: 1px solid var(--line);
|
|
359
|
+
}
|
|
360
|
+
.message.error { border-color: rgba(178, 65, 45, 0.35); }
|
|
361
|
+
.message-meta {
|
|
362
|
+
margin-top: 10px;
|
|
363
|
+
font-size: 11px;
|
|
364
|
+
color: var(--muted);
|
|
365
|
+
}
|
|
366
|
+
.composer {
|
|
367
|
+
padding: 18px 20px 20px;
|
|
368
|
+
border-top: 1px solid var(--line);
|
|
369
|
+
}
|
|
370
|
+
.composer textarea,
|
|
371
|
+
.field textarea,
|
|
372
|
+
.field input,
|
|
373
|
+
.field select {
|
|
374
|
+
width: 100%;
|
|
375
|
+
border-radius: 14px;
|
|
376
|
+
border: 1px solid var(--line);
|
|
377
|
+
background: var(--panel-2);
|
|
378
|
+
color: var(--text);
|
|
379
|
+
outline: none;
|
|
380
|
+
padding: 12px 14px;
|
|
381
|
+
}
|
|
382
|
+
.composer textarea { min-height: 112px; resize: vertical; }
|
|
383
|
+
.field {
|
|
384
|
+
margin-bottom: 14px;
|
|
385
|
+
}
|
|
386
|
+
.field label {
|
|
387
|
+
display: block;
|
|
388
|
+
margin-bottom: 8px;
|
|
389
|
+
font-size: 12px;
|
|
390
|
+
text-transform: uppercase;
|
|
391
|
+
letter-spacing: 0.08em;
|
|
392
|
+
color: var(--muted);
|
|
393
|
+
}
|
|
394
|
+
.form-grid {
|
|
395
|
+
display: grid;
|
|
396
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
397
|
+
gap: 14px;
|
|
398
|
+
}
|
|
399
|
+
.inline-actions {
|
|
400
|
+
display: flex;
|
|
401
|
+
gap: 10px;
|
|
402
|
+
flex-wrap: wrap;
|
|
403
|
+
}
|
|
404
|
+
.notice {
|
|
405
|
+
border-radius: 16px;
|
|
406
|
+
padding: 14px 16px;
|
|
407
|
+
background: rgba(208, 110, 43, 0.08);
|
|
408
|
+
border: 1px solid rgba(208, 110, 43, 0.18);
|
|
409
|
+
color: var(--muted);
|
|
410
|
+
line-height: 1.6;
|
|
411
|
+
}
|
|
412
|
+
.task-list {
|
|
413
|
+
display: grid;
|
|
414
|
+
gap: 12px;
|
|
415
|
+
}
|
|
416
|
+
.task-card {
|
|
417
|
+
border: 1px solid var(--line);
|
|
418
|
+
background: var(--panel-2);
|
|
419
|
+
border-radius: 16px;
|
|
420
|
+
padding: 16px;
|
|
421
|
+
}
|
|
422
|
+
.task-top {
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: flex-start;
|
|
425
|
+
justify-content: space-between;
|
|
426
|
+
gap: 12px;
|
|
427
|
+
}
|
|
428
|
+
.task-title {
|
|
429
|
+
font-size: 15px;
|
|
430
|
+
font-weight: 700;
|
|
431
|
+
}
|
|
432
|
+
.task-prompt {
|
|
433
|
+
margin-top: 10px;
|
|
434
|
+
color: var(--muted);
|
|
435
|
+
line-height: 1.6;
|
|
436
|
+
font-size: 13px;
|
|
437
|
+
white-space: pre-wrap;
|
|
438
|
+
}
|
|
439
|
+
.task-meta {
|
|
440
|
+
margin-top: 12px;
|
|
441
|
+
display: flex;
|
|
442
|
+
gap: 10px;
|
|
443
|
+
flex-wrap: wrap;
|
|
444
|
+
font-size: 12px;
|
|
445
|
+
color: var(--muted);
|
|
446
|
+
}
|
|
447
|
+
.tool-grid {
|
|
448
|
+
display: grid;
|
|
449
|
+
gap: 12px;
|
|
450
|
+
}
|
|
451
|
+
.tool-card {
|
|
452
|
+
border: 1px solid var(--line);
|
|
453
|
+
background: var(--panel-2);
|
|
454
|
+
border-radius: 18px;
|
|
455
|
+
padding: 16px;
|
|
456
|
+
}
|
|
457
|
+
.tool-head {
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: flex-start;
|
|
460
|
+
justify-content: space-between;
|
|
461
|
+
gap: 14px;
|
|
462
|
+
}
|
|
463
|
+
.tool-name {
|
|
464
|
+
font-size: 16px;
|
|
465
|
+
font-weight: 700;
|
|
466
|
+
}
|
|
467
|
+
.tool-hint {
|
|
468
|
+
margin-top: 8px;
|
|
469
|
+
font-size: 13px;
|
|
470
|
+
color: var(--muted);
|
|
471
|
+
line-height: 1.6;
|
|
472
|
+
}
|
|
473
|
+
.tool-actions {
|
|
474
|
+
margin-top: 16px;
|
|
475
|
+
display: flex;
|
|
476
|
+
gap: 10px;
|
|
477
|
+
flex-wrap: wrap;
|
|
478
|
+
}
|
|
479
|
+
.table {
|
|
480
|
+
display: grid;
|
|
481
|
+
gap: 12px;
|
|
482
|
+
}
|
|
483
|
+
.row {
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
justify-content: space-between;
|
|
487
|
+
gap: 12px;
|
|
488
|
+
padding: 12px 0;
|
|
489
|
+
border-bottom: 1px solid var(--line);
|
|
490
|
+
}
|
|
491
|
+
.row:last-child { border-bottom: 0; }
|
|
492
|
+
.row-key {
|
|
493
|
+
color: var(--muted);
|
|
494
|
+
font-size: 13px;
|
|
495
|
+
}
|
|
496
|
+
.row-value {
|
|
497
|
+
font-family: var(--font-mono);
|
|
498
|
+
font-size: 13px;
|
|
499
|
+
}
|
|
500
|
+
.empty {
|
|
501
|
+
padding: 32px 12px;
|
|
502
|
+
text-align: center;
|
|
503
|
+
color: var(--muted);
|
|
504
|
+
}
|
|
505
|
+
.console-drawer {
|
|
506
|
+
position: fixed;
|
|
507
|
+
right: 22px;
|
|
508
|
+
bottom: 22px;
|
|
509
|
+
width: min(760px, calc(100vw - 44px));
|
|
510
|
+
border-radius: 18px;
|
|
511
|
+
border: 1px solid var(--line);
|
|
512
|
+
background: #14110f;
|
|
513
|
+
color: #e9e2d9;
|
|
514
|
+
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.35);
|
|
515
|
+
overflow: hidden;
|
|
516
|
+
display: none;
|
|
517
|
+
}
|
|
518
|
+
.console-drawer.open { display: block; }
|
|
519
|
+
.console-bar {
|
|
520
|
+
display: flex;
|
|
521
|
+
align-items: center;
|
|
522
|
+
justify-content: space-between;
|
|
523
|
+
padding: 12px 16px;
|
|
524
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
525
|
+
}
|
|
526
|
+
.console-log {
|
|
527
|
+
padding: 14px 16px;
|
|
528
|
+
font-family: var(--font-mono);
|
|
529
|
+
font-size: 12px;
|
|
530
|
+
line-height: 1.6;
|
|
531
|
+
max-height: 340px;
|
|
532
|
+
overflow: auto;
|
|
533
|
+
white-space: pre-wrap;
|
|
534
|
+
}
|
|
535
|
+
.console-log .ok { color: #67d397; }
|
|
536
|
+
.console-log .error { color: #ff8f7a; }
|
|
537
|
+
.console-log .warn { color: #f3c269; }
|
|
538
|
+
.footer-note {
|
|
539
|
+
margin-top: 24px;
|
|
540
|
+
text-align: center;
|
|
541
|
+
color: var(--muted);
|
|
542
|
+
font-size: 12px;
|
|
543
|
+
}
|
|
544
|
+
@media (max-width: 1080px) {
|
|
545
|
+
.app-shell { grid-template-columns: 1fr; }
|
|
546
|
+
.sidebar { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
547
|
+
.workspace-shell { grid-template-columns: 1fr; }
|
|
548
|
+
.cards-3, .form-grid { grid-template-columns: 1fr; }
|
|
549
|
+
}
|
|
139
550
|
</style>
|
|
140
551
|
</head>
|
|
141
552
|
<body>
|
|
142
|
-
<div class="app"
|
|
143
|
-
<
|
|
144
|
-
<div class="
|
|
145
|
-
<
|
|
146
|
-
|
|
553
|
+
<div class="app-shell">
|
|
554
|
+
<aside class="sidebar">
|
|
555
|
+
<div class="brand">
|
|
556
|
+
<div>
|
|
557
|
+
<div class="brand-title"><span>HolySheep</span> Workspace</div>
|
|
558
|
+
<div class="brand-sub">AionUi-style Web cockpit for HolySheep CLI</div>
|
|
559
|
+
</div>
|
|
147
560
|
</div>
|
|
148
|
-
<div class="
|
|
149
|
-
<
|
|
150
|
-
<
|
|
561
|
+
<div class="nav-group">
|
|
562
|
+
<div class="nav-label">Workspace</div>
|
|
563
|
+
<button class="nav-item" data-route="dashboard"><span class="nav-dot"></span><span>Dashboard</span></button>
|
|
564
|
+
<button class="nav-item" data-route="workspace"><span class="nav-dot"></span><span>Conversations</span></button>
|
|
565
|
+
<button class="nav-item" data-route="tasks"><span class="nav-dot"></span><span>Scheduled Tasks</span></button>
|
|
566
|
+
<button class="nav-item" data-route="tools"><span class="nav-dot"></span><span>Tools & MCP</span></button>
|
|
567
|
+
<button class="nav-item" data-route="account"><span class="nav-dot"></span><span>HolySheep Account</span></button>
|
|
151
568
|
</div>
|
|
152
|
-
|
|
569
|
+
<div class="sidebar-card">
|
|
570
|
+
<h4>Why API config lives under tasks</h4>
|
|
571
|
+
<p>The HolySheep API section under Scheduled Tasks is the default runtime config for workspace conversations and task execution. Without it, the workspace cannot run model-backed flows.</p>
|
|
572
|
+
</div>
|
|
573
|
+
</aside>
|
|
153
574
|
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
575
|
+
<main class="main">
|
|
576
|
+
<div class="topbar">
|
|
577
|
+
<div class="topbar-title">
|
|
578
|
+
<h1 id="page-title">Dashboard</h1>
|
|
579
|
+
<p id="page-subtitle">Modern local workspace for HolySheep CLI.</p>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="topbar-actions">
|
|
582
|
+
<div class="pill" id="status-pill">Loading...</div>
|
|
583
|
+
<button class="btn" id="refresh-btn">Refresh</button>
|
|
584
|
+
</div>
|
|
160
585
|
</div>
|
|
161
|
-
|
|
586
|
+
|
|
587
|
+
<div id="page-content"></div>
|
|
588
|
+
<div class="footer-note">`hs web` now hosts the HolySheep workspace shell, task scheduler, and API-backed conversations.</div>
|
|
589
|
+
</main>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<div class="console-drawer" id="console-drawer">
|
|
593
|
+
<div class="console-bar">
|
|
594
|
+
<strong id="console-title">Activity Log</strong>
|
|
595
|
+
<button class="btn" id="console-close">Close</button>
|
|
162
596
|
</div>
|
|
163
|
-
<div
|
|
164
|
-
<div id="footer-section"></div>
|
|
597
|
+
<div class="console-log" id="console-log"></div>
|
|
165
598
|
</div>
|
|
166
599
|
|
|
167
600
|
<script>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
hotReload: '已生效,无需重启', needRestart: '重启终端后生效',
|
|
190
|
-
launch: '启动命令', upgradeOne: '升级', rollback: '回退版本', open: '打开',
|
|
191
|
-
updateAvailable: '有新版本可用', updateNow: '立即升级',
|
|
601
|
+
const state = {
|
|
602
|
+
route: 'dashboard',
|
|
603
|
+
status: null,
|
|
604
|
+
balance: null,
|
|
605
|
+
models: [],
|
|
606
|
+
tools: [],
|
|
607
|
+
workspace: null,
|
|
608
|
+
currentConversationId: null,
|
|
609
|
+
currentConversation: null,
|
|
610
|
+
searchQuery: '',
|
|
611
|
+
searchResults: null,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const pageMeta = {
|
|
615
|
+
dashboard: {
|
|
616
|
+
title: 'Dashboard',
|
|
617
|
+
subtitle: 'AionUi-inspired operational overview for HolySheep tooling, conversations, and scheduled runs.',
|
|
618
|
+
},
|
|
619
|
+
workspace: {
|
|
620
|
+
title: 'Conversations',
|
|
621
|
+
subtitle: 'Search from the upper-left, jump into sessions, and chat through the configured HolySheep API runtime.',
|
|
192
622
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
recharge: 'Recharge', register: 'No account? Register',
|
|
197
|
-
apiKeyPlaceholder: 'Enter API Key (cr_xxx)',
|
|
198
|
-
tools: 'AI Tools', toolsHint: 'One-click setup for HolySheep API', upgrade: 'Upgrade Tools',
|
|
199
|
-
installed: 'Installed', notInstalled: 'Not installed', configured: 'Configured', notConfigured: 'Not configured',
|
|
200
|
-
configure: 'Configure', reconfigure: 'Reconfigure', reset: 'Reset', install: 'Install',
|
|
201
|
-
installManual: 'Manual install',
|
|
202
|
-
env: 'Environment Variables', cleanConflicts: 'Clean Conflicts', cleaning: 'Cleaning...',
|
|
203
|
-
set: 'Set', notSet: 'Not set',
|
|
204
|
-
shellConfig: 'Shell Config', managedBlock: 'managed block', noManagedBlock: 'no managed block',
|
|
205
|
-
docs: 'Docs', pricing: 'Pricing', support: 'Support',
|
|
206
|
-
slogan: 'Official Claude / GPT / Gemini API Proxy',
|
|
207
|
-
checking: 'Loading...', close: 'Close', log: 'Activity Log',
|
|
208
|
-
confirmReset: 'Reset HolySheep config for this tool?',
|
|
209
|
-
configSuccess: 'Configured', configFailed: 'Config failed',
|
|
210
|
-
installSuccess: 'Installed', installFailed: 'Install failed',
|
|
211
|
-
needLogin: 'Please login first', cleanDone: 'Cleaned',
|
|
212
|
-
hotReload: 'Active, no restart needed', needRestart: 'Restart terminal to apply',
|
|
213
|
-
launch: 'Launch', upgradeOne: 'Upgrade', rollback: 'Rollback', open: 'Open',
|
|
214
|
-
updateAvailable: 'Update available', updateNow: 'Update now',
|
|
623
|
+
tasks: {
|
|
624
|
+
title: 'Scheduled Tasks',
|
|
625
|
+
subtitle: 'Manage recurring prompts and maintain the default HolySheep API config directly below the task list.',
|
|
215
626
|
},
|
|
627
|
+
tools: {
|
|
628
|
+
title: 'Tools & MCP',
|
|
629
|
+
subtitle: 'Inspect local CLI adapters, launch tools, and run one-click configuration from the same shell.',
|
|
630
|
+
},
|
|
631
|
+
account: {
|
|
632
|
+
title: 'HolySheep Account',
|
|
633
|
+
subtitle: 'Keep authentication, balance, environment, and doctor output visible in one place.',
|
|
634
|
+
},
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const toolNames = {
|
|
638
|
+
'claude-code': 'Claude Code',
|
|
639
|
+
codex: 'Codex CLI',
|
|
640
|
+
droid: 'Droid',
|
|
641
|
+
opencode: 'OpenCode',
|
|
642
|
+
openclaw: 'OpenClaw',
|
|
643
|
+
'env-config': 'Env Config',
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function esc(value) {
|
|
647
|
+
return String(value == null ? '' : value)
|
|
648
|
+
.replace(/&/g, '&')
|
|
649
|
+
.replace(/</g, '<')
|
|
650
|
+
.replace(/>/g, '>')
|
|
651
|
+
.replace(/"/g, '"')
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function formatDate(value) {
|
|
655
|
+
if (!value) return 'Never'
|
|
656
|
+
const date = new Date(value)
|
|
657
|
+
if (Number.isNaN(date.getTime())) return 'Never'
|
|
658
|
+
return date.toLocaleString()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function api(path, options) {
|
|
662
|
+
return fetch(path.startsWith('/api/') ? path : `/api/${path}`, options).then(async (response) => {
|
|
663
|
+
const payload = await response.json().catch(() => ({}))
|
|
664
|
+
if (!response.ok) {
|
|
665
|
+
const error = new Error(payload.error || payload.message || `HTTP ${response.status}`)
|
|
666
|
+
error.payload = payload
|
|
667
|
+
throw error
|
|
668
|
+
}
|
|
669
|
+
return payload
|
|
670
|
+
})
|
|
216
671
|
}
|
|
217
672
|
|
|
218
|
-
|
|
219
|
-
const
|
|
673
|
+
async function loadBaseState() {
|
|
674
|
+
const [status, balance, tools, workspace, models] = await Promise.all([
|
|
675
|
+
api('status').catch(() => null),
|
|
676
|
+
api('balance').catch(() => null),
|
|
677
|
+
api('tools').catch(() => []),
|
|
678
|
+
api('workspace/state').catch(() => ({ conversations: [], scheduledTasks: [], holySheepApi: { ready: false }, tools: [] })),
|
|
679
|
+
api('models').catch(() => []),
|
|
680
|
+
])
|
|
681
|
+
state.status = status
|
|
682
|
+
state.balance = balance
|
|
683
|
+
state.tools = tools
|
|
684
|
+
state.workspace = workspace
|
|
685
|
+
state.models = models
|
|
686
|
+
|
|
687
|
+
if (!state.currentConversationId && workspace.conversations[0]) {
|
|
688
|
+
state.currentConversationId = workspace.conversations[0].id
|
|
689
|
+
}
|
|
690
|
+
if (state.currentConversationId) {
|
|
691
|
+
await loadConversation(state.currentConversationId)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
220
694
|
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
695
|
+
async function loadConversation(id) {
|
|
696
|
+
if (!id) {
|
|
697
|
+
state.currentConversation = null
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
const conversation = await api(`workspace/conversations/${id}`).catch(() => null)
|
|
701
|
+
state.currentConversationId = conversation ? id : null
|
|
702
|
+
state.currentConversation = conversation
|
|
225
703
|
}
|
|
226
704
|
|
|
227
|
-
|
|
228
|
-
|
|
705
|
+
function updateTopbar() {
|
|
706
|
+
const meta = pageMeta[state.route] || pageMeta.dashboard
|
|
707
|
+
document.getElementById('page-title').textContent = meta.title
|
|
708
|
+
document.getElementById('page-subtitle').textContent = meta.subtitle
|
|
709
|
+
document.getElementById('status-pill').textContent = state.workspace?.holySheepApi?.ready
|
|
710
|
+
? `HolySheep API ready · ${state.workspace.holySheepApi.model}`
|
|
711
|
+
: 'HolySheep API not configured'
|
|
712
|
+
}
|
|
229
713
|
|
|
230
|
-
|
|
231
|
-
|
|
714
|
+
function routeFromHash() {
|
|
715
|
+
const hash = window.location.hash.replace(/^#\/?/, '')
|
|
716
|
+
return hash || 'dashboard'
|
|
717
|
+
}
|
|
232
718
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
loadTools()
|
|
238
|
-
loadEnv()
|
|
239
|
-
renderFooter()
|
|
240
|
-
startUpdateChecker()
|
|
719
|
+
function setRoute(route) {
|
|
720
|
+
state.route = pageMeta[route] ? route : 'dashboard'
|
|
721
|
+
window.location.hash = `#/${state.route}`
|
|
722
|
+
render()
|
|
241
723
|
}
|
|
242
724
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const s = await api('status')
|
|
250
|
-
if (s.updateAvailable) {
|
|
251
|
-
document.getElementById('version').innerHTML = `v${s.version} <span style="color:var(--primary);cursor:pointer;font-weight:600" onclick="doUpgradeTool('holysheep','HolySheep CLI')" title="${t('updateNow')}"> → v${s.updateAvailable} ${t('updateNow')}</span>`
|
|
252
|
-
}
|
|
253
|
-
} catch {}
|
|
254
|
-
}, 5 * 60 * 1000)
|
|
725
|
+
function bindNav() {
|
|
726
|
+
document.querySelectorAll('.nav-item').forEach((item) => {
|
|
727
|
+
item.classList.toggle('active', item.dataset.route === state.route)
|
|
728
|
+
item.onclick = () => setRoute(item.dataset.route)
|
|
729
|
+
})
|
|
255
730
|
}
|
|
256
731
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
732
|
+
function renderDashboard() {
|
|
733
|
+
const status = state.status || {}
|
|
734
|
+
const balance = state.balance || {}
|
|
735
|
+
const workspace = state.workspace || { conversations: [], scheduledTasks: [], holySheepApi: { ready: false } }
|
|
736
|
+
|
|
737
|
+
return `
|
|
738
|
+
<div class="grid cards-3">
|
|
739
|
+
${renderMetricCard('Conversations', workspace.conversations.length, 'Saved workspace sessions')}
|
|
740
|
+
${renderMetricCard('Scheduled Tasks', workspace.scheduledTasks.length, 'Recurring or one-shot prompts')}
|
|
741
|
+
${renderMetricCard('API Runtime', workspace.holySheepApi.ready ? 'Ready' : 'Missing', workspace.holySheepApi.ready ? workspace.holySheepApi.model : 'Configure on the tasks page')}
|
|
742
|
+
</div>
|
|
743
|
+
<div class="grid" style="grid-template-columns: 1.2fr 0.8fr; margin-top: 18px;">
|
|
744
|
+
<section class="panel">
|
|
745
|
+
<div class="panel-header">
|
|
746
|
+
<div>
|
|
747
|
+
<h2>Workspace Snapshot</h2>
|
|
748
|
+
<p>Account state, balance, and the most recent conversations.</p>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
<div class="panel-body">
|
|
752
|
+
<div class="table">
|
|
753
|
+
<div class="row"><div class="row-key">Logged in</div><div class="row-value">${status.loggedIn ? 'Yes' : 'No'}</div></div>
|
|
754
|
+
<div class="row"><div class="row-key">Saved API key</div><div class="row-value">${esc(status.apiKey || 'Not set')}</div></div>
|
|
755
|
+
<div class="row"><div class="row-key">Balance</div><div class="row-value">${balance.balance != null ? `$${Number(balance.balance).toFixed(2)}` : 'Unavailable'}</div></div>
|
|
756
|
+
<div class="row"><div class="row-key">Today cost</div><div class="row-value">${balance.todayCost != null ? `$${Number(balance.todayCost).toFixed(2)}` : 'Unavailable'}</div></div>
|
|
757
|
+
<div class="row"><div class="row-key">Month cost</div><div class="row-value">${balance.monthCost != null ? `$${Number(balance.monthCost).toFixed(2)}` : 'Unavailable'}</div></div>
|
|
758
|
+
</div>
|
|
759
|
+
<div style="margin-top:18px;">
|
|
760
|
+
<h3 style="margin:0 0 10px;">Recent conversations</h3>
|
|
761
|
+
${workspace.conversations.length ? workspace.conversations.slice(0, 4).map((conversation) => `
|
|
762
|
+
<div class="task-card" style="margin-bottom:10px;">
|
|
763
|
+
<div class="task-top">
|
|
764
|
+
<div>
|
|
765
|
+
<div class="task-title">${esc(conversation.title)}</div>
|
|
766
|
+
<div class="task-meta">
|
|
767
|
+
<span>${esc(toolNames[conversation.toolId] || conversation.toolId)}</span>
|
|
768
|
+
<span>${formatDate(conversation.updatedAt)}</span>
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
<button class="btn" onclick="openConversation('${conversation.id}')">Open</button>
|
|
772
|
+
</div>
|
|
773
|
+
<div class="task-prompt">${esc(conversation.summary || 'No summary yet')}</div>
|
|
774
|
+
</div>
|
|
775
|
+
`).join('') : '<div class="empty">No conversations yet. Create one from the Conversations page.</div>'}
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
</section>
|
|
779
|
+
<section class="panel">
|
|
780
|
+
<div class="panel-header">
|
|
781
|
+
<div>
|
|
782
|
+
<h2>Default Task Runtime</h2>
|
|
783
|
+
<p>The task page owns the default HolySheep API config used by conversations and scheduled jobs.</p>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
<div class="panel-body">
|
|
787
|
+
<div class="notice">
|
|
788
|
+
<strong>Status:</strong> ${workspace.holySheepApi.ready ? 'Configured' : 'Missing'}<br>
|
|
789
|
+
<strong>Base URL:</strong> ${esc(workspace.holySheepApi.baseUrl || 'Not configured')}<br>
|
|
790
|
+
<strong>Model:</strong> ${esc(workspace.holySheepApi.model || 'Not configured')}
|
|
791
|
+
</div>
|
|
792
|
+
<div style="margin-top:16px;" class="inline-actions">
|
|
793
|
+
<button class="btn primary" onclick="setRoute('tasks')">Open Scheduled Tasks</button>
|
|
794
|
+
<button class="btn" onclick="setRoute('tools')">Manage CLI tools</button>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
</section>
|
|
798
|
+
</div>
|
|
799
|
+
`
|
|
261
800
|
}
|
|
262
801
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
802
|
+
function renderMetricCard(label, value, caption) {
|
|
803
|
+
return `
|
|
804
|
+
<section class="panel metric">
|
|
805
|
+
<div class="metric-value">${esc(value)}</div>
|
|
806
|
+
<div class="metric-label">${esc(label)}</div>
|
|
807
|
+
<div class="metric-label" style="margin-top:12px;">${esc(caption)}</div>
|
|
808
|
+
</section>
|
|
809
|
+
`
|
|
810
|
+
}
|
|
267
811
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
812
|
+
function renderConversationRail(workspace) {
|
|
813
|
+
const searchBlock = state.searchQuery.trim()
|
|
814
|
+
? `
|
|
815
|
+
<div class="search-result-block">
|
|
816
|
+
<div class="search-result-title">Search results</div>
|
|
817
|
+
${(state.searchResults?.conversations || []).map((item) => `
|
|
818
|
+
<button class="search-result-item" onclick="openConversation('${item.id}')">
|
|
819
|
+
<strong>${esc(item.title)}</strong><br>
|
|
820
|
+
<span style="color:var(--muted);font-size:12px;">${esc(item.summary || '')}</span>
|
|
821
|
+
</button>
|
|
822
|
+
`).join('') || '<div class="empty">No matching conversations.</div>'}
|
|
823
|
+
</div>
|
|
824
|
+
`
|
|
825
|
+
: ''
|
|
271
826
|
|
|
272
|
-
|
|
827
|
+
const list = workspace.conversations.length
|
|
828
|
+
? workspace.conversations.map((conversation) => `
|
|
829
|
+
<button class="conversation-item ${conversation.id === state.currentConversationId ? 'active' : ''}" onclick="openConversation('${conversation.id}')">
|
|
830
|
+
<div class="conversation-title">${esc(conversation.title)}</div>
|
|
831
|
+
<div class="conversation-meta">
|
|
832
|
+
${esc(toolNames[conversation.toolId] || conversation.toolId)} · ${formatDate(conversation.updatedAt)}<br>
|
|
833
|
+
${esc(conversation.summary || 'No summary yet')}
|
|
834
|
+
</div>
|
|
835
|
+
</button>
|
|
836
|
+
`).join('')
|
|
837
|
+
: '<div class="empty">Create your first conversation to begin using the workspace.</div>'
|
|
838
|
+
|
|
839
|
+
return `
|
|
840
|
+
<section class="panel conversation-rail">
|
|
841
|
+
<div class="panel-header">
|
|
842
|
+
<div>
|
|
843
|
+
<h2>Sessions</h2>
|
|
844
|
+
<p>Search in the upper-left, then jump between CLI work sessions.</p>
|
|
845
|
+
</div>
|
|
846
|
+
<button class="btn primary" onclick="createConversation()">New</button>
|
|
847
|
+
</div>
|
|
848
|
+
<div class="panel-body" style="padding-top:0;">
|
|
849
|
+
<div class="search-box">
|
|
850
|
+
<span>Search</span>
|
|
851
|
+
<input id="conversation-search" placeholder="Search titles and message text" value="${esc(state.searchQuery)}">
|
|
852
|
+
</div>
|
|
853
|
+
${searchBlock}
|
|
854
|
+
</div>
|
|
855
|
+
<div class="conversation-list">${list}</div>
|
|
856
|
+
</section>
|
|
857
|
+
`
|
|
858
|
+
}
|
|
273
859
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
860
|
+
function renderConversationStage(workspace) {
|
|
861
|
+
const conversation = state.currentConversation
|
|
862
|
+
const ready = workspace.holySheepApi.ready
|
|
863
|
+
if (!conversation) {
|
|
864
|
+
return `
|
|
865
|
+
<section class="panel conversation-stage">
|
|
866
|
+
<div class="stage-toolbar">
|
|
867
|
+
<div class="notice">No conversation selected. Create one from the left rail.</div>
|
|
868
|
+
</div>
|
|
869
|
+
</section>
|
|
870
|
+
`
|
|
277
871
|
}
|
|
278
872
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
873
|
+
const toolOptions = workspace.tools.map((tool) => `
|
|
874
|
+
<option value="${esc(tool.id)}" ${tool.id === conversation.toolId ? 'selected' : ''}>${esc(tool.name)}</option>
|
|
875
|
+
`).join('')
|
|
876
|
+
|
|
877
|
+
const messages = conversation.messages.length
|
|
878
|
+
? conversation.messages.map((message) => `
|
|
879
|
+
<div class="message ${message.role} ${message.status === 'error' ? 'error' : ''}">
|
|
880
|
+
${esc(message.content)}
|
|
881
|
+
<div class="message-meta">${message.role === 'assistant' ? 'Assistant' : 'User'} · ${formatDate(message.createdAt)}${message.meta?.model ? ` · ${esc(message.meta.model)}` : ''}</div>
|
|
286
882
|
</div>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
883
|
+
`).join('')
|
|
884
|
+
: '<div class="empty">This conversation is empty. Send a prompt to create the first turn.</div>'
|
|
885
|
+
|
|
886
|
+
return `
|
|
887
|
+
<section class="panel conversation-stage">
|
|
888
|
+
<div class="stage-toolbar">
|
|
889
|
+
<div class="stage-toolbar-left">
|
|
890
|
+
<h3>${esc(conversation.title)}</h3>
|
|
891
|
+
<span class="badge ${ready ? 'ok' : 'warn'}">${ready ? 'HolySheep API Ready' : 'Configure API on task page'}</span>
|
|
892
|
+
<span class="badge">${esc(toolNames[conversation.toolId] || conversation.toolId)}</span>
|
|
893
|
+
</div>
|
|
894
|
+
<div class="inline-actions">
|
|
895
|
+
<select id="conversation-tool-select">${toolOptions}</select>
|
|
896
|
+
<button class="btn" onclick="launchCurrentTool()">Launch Tool</button>
|
|
897
|
+
</div>
|
|
297
898
|
</div>
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
<
|
|
303
|
-
|
|
899
|
+
<div class="message-list" id="message-list">${messages}</div>
|
|
900
|
+
<div class="composer">
|
|
901
|
+
${ready ? '' : '<div class="notice" style="margin-bottom:14px;">The workspace runtime uses the HolySheep API config from the Scheduled Tasks page. Configure API key, base URL, and model there before sending messages.</div>'}
|
|
902
|
+
<textarea id="conversation-input" placeholder="Ask for code changes, debugging steps, or tool usage..." ${ready ? '' : 'disabled'}></textarea>
|
|
903
|
+
<div class="inline-actions" style="margin-top:12px;">
|
|
904
|
+
<button class="btn primary" onclick="sendConversationMessage()" ${ready ? '' : 'disabled'}>Send</button>
|
|
905
|
+
<button class="btn" onclick="renameConversation()">Rename</button>
|
|
906
|
+
<button class="btn" onclick="togglePinConversation()">${conversation.pinned ? 'Unpin' : 'Pin'}</button>
|
|
907
|
+
</div>
|
|
304
908
|
</div>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
909
|
+
</section>
|
|
910
|
+
`
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function renderWorkspace() {
|
|
914
|
+
const workspace = state.workspace || { conversations: [], tools: [], holySheepApi: { ready: false } }
|
|
915
|
+
return `
|
|
916
|
+
<div class="workspace-shell">
|
|
917
|
+
${renderConversationRail(workspace)}
|
|
918
|
+
${renderConversationStage(workspace)}
|
|
919
|
+
</div>
|
|
920
|
+
`
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function renderTasks() {
|
|
924
|
+
const workspace = state.workspace || { scheduledTasks: [], holySheepApi: { ready: false } }
|
|
925
|
+
const taskCards = workspace.scheduledTasks.length
|
|
926
|
+
? workspace.scheduledTasks.map((task) => `
|
|
927
|
+
<div class="task-card">
|
|
928
|
+
<div class="task-top">
|
|
929
|
+
<div>
|
|
930
|
+
<div class="task-title">${esc(task.title)}</div>
|
|
931
|
+
<div class="task-meta">
|
|
932
|
+
<span>${esc(task.schedule)}</span>
|
|
933
|
+
<span>${task.active ? 'Active' : 'Paused'}</span>
|
|
934
|
+
<span>Last run: ${formatDate(task.lastRunAt)}</span>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="inline-actions">
|
|
938
|
+
<button class="btn" onclick="runTask('${task.id}')">Run now</button>
|
|
939
|
+
<button class="btn" onclick="editTask('${task.id}')">Edit</button>
|
|
940
|
+
<button class="btn danger" onclick="deleteTask('${task.id}')">Delete</button>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
<div class="task-prompt">${esc(task.prompt)}</div>
|
|
944
|
+
<div class="task-meta">
|
|
945
|
+
<span>Status: ${esc(task.lastStatus || 'idle')}</span>
|
|
946
|
+
<span>${esc(task.lastResult ? task.lastResult.slice(0, 160) : 'No result yet')}</span>
|
|
947
|
+
</div>
|
|
308
948
|
</div>
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
949
|
+
`).join('')
|
|
950
|
+
: '<div class="empty">No scheduled tasks yet. Create one to run recurring HolySheep prompts.</div>'
|
|
951
|
+
|
|
952
|
+
return `
|
|
953
|
+
<div class="grid" style="grid-template-columns: 1.15fr 0.85fr;">
|
|
954
|
+
<section class="panel">
|
|
955
|
+
<div class="panel-header">
|
|
956
|
+
<div>
|
|
957
|
+
<h2>Tasks</h2>
|
|
958
|
+
<p>Create recurring prompts. They inherit the HolySheep API configuration below.</p>
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
<div class="panel-body">
|
|
962
|
+
<div class="field">
|
|
963
|
+
<label>Task title</label>
|
|
964
|
+
<input id="task-title" placeholder="Nightly code review summary">
|
|
965
|
+
</div>
|
|
966
|
+
<div class="field">
|
|
967
|
+
<label>Task prompt</label>
|
|
968
|
+
<textarea id="task-prompt" rows="5" placeholder="Summarize yesterday's code changes and highlight risky diffs."></textarea>
|
|
969
|
+
</div>
|
|
970
|
+
<div class="form-grid">
|
|
971
|
+
<div class="field">
|
|
972
|
+
<label>Schedule</label>
|
|
973
|
+
<input id="task-schedule" placeholder="5m / 1h / 1d">
|
|
974
|
+
</div>
|
|
975
|
+
<div class="field">
|
|
976
|
+
<label>Model override</label>
|
|
977
|
+
<select id="task-model-override">
|
|
978
|
+
<option value="">Use default HolySheep model</option>
|
|
979
|
+
${state.models.map((model) => `<option value="${esc(model.id)}">${esc(model.label)}</option>`).join('')}
|
|
980
|
+
</select>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
<div class="inline-actions">
|
|
984
|
+
<button class="btn primary" onclick="saveTask()">Save task</button>
|
|
985
|
+
<button class="btn" onclick="clearTaskForm()">Clear</button>
|
|
986
|
+
</div>
|
|
987
|
+
<div style="margin-top:18px;" class="task-list">${taskCards}</div>
|
|
988
|
+
</div>
|
|
989
|
+
</section>
|
|
990
|
+
<section class="panel">
|
|
991
|
+
<div class="panel-header">
|
|
992
|
+
<div>
|
|
993
|
+
<h2>HolySheep API</h2>
|
|
994
|
+
<p>This section sits under Scheduled Tasks on purpose: tasks and workspace conversations both depend on it.</p>
|
|
995
|
+
</div>
|
|
996
|
+
</div>
|
|
997
|
+
<div class="panel-body">
|
|
998
|
+
<div class="notice" style="margin-bottom:14px;">
|
|
999
|
+
Without this config, the workspace cannot run model-backed conversations or scheduled tasks.
|
|
1000
|
+
</div>
|
|
1001
|
+
<div class="field">
|
|
1002
|
+
<label>HolySheep API Key</label>
|
|
1003
|
+
<input type="password" id="runtime-api-key" placeholder="cr_xxx" value="${esc(workspace.holySheepApi.apiKey || '')}">
|
|
1004
|
+
</div>
|
|
1005
|
+
<div class="field">
|
|
1006
|
+
<label>Base URL</label>
|
|
1007
|
+
<input id="runtime-base-url" placeholder="https://api.holysheep.ai/v1" value="${esc(workspace.holySheepApi.baseUrl || '')}">
|
|
1008
|
+
</div>
|
|
1009
|
+
<div class="field">
|
|
1010
|
+
<label>Default model</label>
|
|
1011
|
+
<select id="runtime-model">
|
|
1012
|
+
<option value="">Select model</option>
|
|
1013
|
+
${state.models.map((model) => `<option value="${esc(model.id)}" ${workspace.holySheepApi.model === model.id ? 'selected' : ''}>${esc(model.label)}</option>`).join('')}
|
|
1014
|
+
</select>
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="inline-actions">
|
|
1017
|
+
<button class="btn primary" onclick="saveRuntimeConfig()">Save runtime config</button>
|
|
1018
|
+
<button class="btn" onclick="copyLoginKey()">Use saved login key</button>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="task-meta" style="margin-top:14px;">
|
|
1021
|
+
<span>Ready: ${workspace.holySheepApi.ready ? 'Yes' : 'No'}</span>
|
|
1022
|
+
<span>Model: ${esc(workspace.holySheepApi.model || 'Not configured')}</span>
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
</section>
|
|
1026
|
+
</div>
|
|
1027
|
+
`
|
|
312
1028
|
}
|
|
313
1029
|
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
1030
|
+
function renderTools() {
|
|
1031
|
+
const tools = state.tools || []
|
|
1032
|
+
return `
|
|
1033
|
+
<section class="panel">
|
|
1034
|
+
<div class="panel-header">
|
|
1035
|
+
<div>
|
|
1036
|
+
<h2>Tools & MCP</h2>
|
|
1037
|
+
<p>Use the existing HolySheep automation endpoints from inside the new workspace shell.</p>
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="panel-body tool-grid">
|
|
1041
|
+
${tools.map((tool) => `
|
|
1042
|
+
<div class="tool-card">
|
|
1043
|
+
<div class="tool-head">
|
|
1044
|
+
<div>
|
|
1045
|
+
<div class="tool-name">${esc(tool.name)}</div>
|
|
1046
|
+
<div class="task-meta">
|
|
1047
|
+
<span class="badge ${tool.installed ? 'ok' : 'warn'}">${tool.installed ? 'Installed' : 'Missing'}</span>
|
|
1048
|
+
<span class="badge ${tool.configured ? 'ok' : 'warn'}">${tool.configured ? 'Configured' : 'Needs setup'}</span>
|
|
1049
|
+
<span>${esc(tool.version || '')}</span>
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
<div class="inline-actions">
|
|
1053
|
+
<button class="btn" onclick="launchTool('${tool.id}')">Launch</button>
|
|
1054
|
+
<button class="btn primary" onclick="configureTool('${tool.id}')">${tool.configured ? 'Reconfigure' : 'Configure'}</button>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
<div class="tool-hint">${esc(tool.hint || 'No additional hint')}</div>
|
|
1058
|
+
</div>
|
|
1059
|
+
`).join('')}
|
|
1060
|
+
</div>
|
|
1061
|
+
</section>
|
|
1062
|
+
`
|
|
325
1063
|
}
|
|
326
1064
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1065
|
+
function renderAccount() {
|
|
1066
|
+
const status = state.status || {}
|
|
1067
|
+
const balance = state.balance || {}
|
|
1068
|
+
return `
|
|
1069
|
+
<div class="grid" style="grid-template-columns: 1fr 1fr;">
|
|
1070
|
+
<section class="panel">
|
|
1071
|
+
<div class="panel-header">
|
|
1072
|
+
<div>
|
|
1073
|
+
<h2>Account</h2>
|
|
1074
|
+
<p>HolySheep login state remains available inside the new workspace shell.</p>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
<div class="panel-body">
|
|
1078
|
+
<div class="field">
|
|
1079
|
+
<label>Saved key</label>
|
|
1080
|
+
<div class="row-value">${esc(status.apiKey || 'Not logged in')}</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
<div class="field">
|
|
1083
|
+
<label>Login with HolySheep API key</label>
|
|
1084
|
+
<input id="login-api-key" type="password" placeholder="cr_xxx">
|
|
1085
|
+
</div>
|
|
1086
|
+
<div class="inline-actions">
|
|
1087
|
+
<button class="btn primary" onclick="login()">Login</button>
|
|
1088
|
+
<button class="btn danger" onclick="logout()">Logout</button>
|
|
1089
|
+
</div>
|
|
1090
|
+
<div class="task-meta" style="margin-top:14px;">
|
|
1091
|
+
<span>Version: ${esc(status.version || 'Unknown')}</span>
|
|
1092
|
+
<span>${status.updateAvailable ? `Update available: ${esc(status.updateAvailable)}` : 'Up to date'}</span>
|
|
1093
|
+
</div>
|
|
1094
|
+
</div>
|
|
1095
|
+
</section>
|
|
1096
|
+
<section class="panel">
|
|
1097
|
+
<div class="panel-header">
|
|
1098
|
+
<div>
|
|
1099
|
+
<h2>Balance</h2>
|
|
1100
|
+
<p>Same data as the legacy hs web panel, preserved inside the AionUi-style shell.</p>
|
|
1101
|
+
</div>
|
|
1102
|
+
</div>
|
|
1103
|
+
<div class="panel-body table">
|
|
1104
|
+
<div class="row"><div class="row-key">Balance</div><div class="row-value">${balance.balance != null ? `$${Number(balance.balance).toFixed(2)}` : 'Unavailable'}</div></div>
|
|
1105
|
+
<div class="row"><div class="row-key">Today cost</div><div class="row-value">${balance.todayCost != null ? `$${Number(balance.todayCost).toFixed(2)}` : 'Unavailable'}</div></div>
|
|
1106
|
+
<div class="row"><div class="row-key">Month cost</div><div class="row-value">${balance.monthCost != null ? `$${Number(balance.monthCost).toFixed(2)}` : 'Unavailable'}</div></div>
|
|
1107
|
+
<div class="row"><div class="row-key">Total calls</div><div class="row-value">${balance.totalCalls != null ? esc(balance.totalCalls) : 'Unavailable'}</div></div>
|
|
1108
|
+
</div>
|
|
1109
|
+
</section>
|
|
1110
|
+
<section class="panel" style="grid-column: 1 / -1;">
|
|
1111
|
+
<div class="panel-header">
|
|
1112
|
+
<div>
|
|
1113
|
+
<h2>Environment Snapshot</h2>
|
|
1114
|
+
<p>High-value information from doctor remains visible without leaving the web workspace.</p>
|
|
1115
|
+
</div>
|
|
1116
|
+
</div>
|
|
1117
|
+
<div class="panel-body" id="doctor-panel">
|
|
1118
|
+
<div class="empty">Click refresh to load doctor state.</div>
|
|
1119
|
+
</div>
|
|
1120
|
+
</section>
|
|
1121
|
+
</div>
|
|
1122
|
+
`
|
|
330
1123
|
}
|
|
331
1124
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
1125
|
+
async function renderDoctorPanel() {
|
|
1126
|
+
const panel = document.getElementById('doctor-panel')
|
|
1127
|
+
if (!panel) return
|
|
1128
|
+
panel.innerHTML = '<div class="empty">Loading doctor output...</div>'
|
|
1129
|
+
const doctor = await api('doctor').catch(() => null)
|
|
1130
|
+
if (!doctor) {
|
|
1131
|
+
panel.innerHTML = '<div class="empty">Doctor output unavailable.</div>'
|
|
1132
|
+
return
|
|
1133
|
+
}
|
|
1134
|
+
panel.innerHTML = `
|
|
1135
|
+
<div class="table">
|
|
1136
|
+
<div class="row"><div class="row-key">Node</div><div class="row-value">${esc(doctor.node?.version || 'Unknown')}</div></div>
|
|
1137
|
+
<div class="row"><div class="row-key">API key</div><div class="row-value">${doctor.apiKey?.set ? esc(doctor.apiKey.masked) : 'Not set'}</div></div>
|
|
1138
|
+
<div class="row"><div class="row-key">Connectivity</div><div class="row-value">${doctor.connectivity?.ok ? `OK (${doctor.connectivity.modelCount} models)` : 'Unavailable'}</div></div>
|
|
1139
|
+
${Array.isArray(doctor.tools) ? doctor.tools.map((tool) => `
|
|
1140
|
+
<div class="row"><div class="row-key">${esc(tool.name)}</div><div class="row-value">${tool.installed ? (tool.configured ? 'Installed + configured' : 'Installed') : 'Missing'}</div></div>
|
|
1141
|
+
`).join('') : ''}
|
|
338
1142
|
</div>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return `<div class="tool-card" id="tool-${tool.id}">
|
|
364
|
-
<div class="tool-dot ${dotClass}"></div>
|
|
365
|
-
<div class="tool-body">
|
|
366
|
-
<div class="tool-name">${esc(tool.name)}</div>
|
|
367
|
-
<div class="tool-meta">${statusBadges} ${hintLine}</div>
|
|
368
|
-
${hintText}
|
|
369
|
-
</div>
|
|
370
|
-
<div class="tool-actions">${actions}</div>
|
|
371
|
-
</div>`
|
|
1143
|
+
`
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function render() {
|
|
1147
|
+
bindNav()
|
|
1148
|
+
updateTopbar()
|
|
1149
|
+
const root = document.getElementById('page-content')
|
|
1150
|
+
if (state.route === 'workspace') {
|
|
1151
|
+
root.innerHTML = renderWorkspace()
|
|
1152
|
+
bindWorkspaceControls()
|
|
1153
|
+
return
|
|
1154
|
+
}
|
|
1155
|
+
if (state.route === 'tasks') {
|
|
1156
|
+
root.innerHTML = renderTasks()
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
if (state.route === 'tools') {
|
|
1160
|
+
root.innerHTML = renderTools()
|
|
1161
|
+
return
|
|
1162
|
+
}
|
|
1163
|
+
if (state.route === 'account') {
|
|
1164
|
+
root.innerHTML = renderAccount()
|
|
1165
|
+
void renderDoctorPanel()
|
|
1166
|
+
return
|
|
372
1167
|
}
|
|
1168
|
+
root.innerHTML = renderDashboard()
|
|
1169
|
+
}
|
|
373
1170
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
1171
|
+
function bindWorkspaceControls() {
|
|
1172
|
+
const searchInput = document.getElementById('conversation-search')
|
|
1173
|
+
if (searchInput) {
|
|
1174
|
+
searchInput.oninput = async (event) => {
|
|
1175
|
+
state.searchQuery = event.target.value
|
|
1176
|
+
if (!state.searchQuery.trim()) {
|
|
1177
|
+
state.searchResults = null
|
|
1178
|
+
render()
|
|
1179
|
+
return
|
|
1180
|
+
}
|
|
1181
|
+
state.searchResults = await api(`workspace/search?q=${encodeURIComponent(state.searchQuery)}`).catch(() => ({ conversations: [], tasks: [] }))
|
|
1182
|
+
render()
|
|
381
1183
|
}
|
|
382
|
-
hintLine = ''
|
|
383
|
-
} else if (!tool.configured) {
|
|
384
|
-
dotClass = 'dot-warn'
|
|
385
|
-
statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-warn">${t('notConfigured')}</span>`
|
|
386
|
-
const upgradeBtn = tool.canUpgrade ? `<button class="btn btn-outline btn-sm" onclick="doUpgradeTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('upgradeOne')}</button>` : ''
|
|
387
|
-
const rollbackBtn = tool.canUpgrade && tool.npmPkg ? `<button class="btn btn-outline btn-sm" onclick="doRollbackTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('rollback')}</button>` : ''
|
|
388
|
-
actions = `<button class="btn btn-primary btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('configure')}</button>${upgradeBtn}${rollbackBtn}`
|
|
389
|
-
hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
|
|
390
|
-
} else {
|
|
391
|
-
dotClass = 'dot-ok'
|
|
392
|
-
statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-ok">${t('configured')}</span>`
|
|
393
|
-
const upgradeBtn = tool.canUpgrade ? `<button class="btn btn-outline btn-sm" onclick="doUpgradeTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('upgradeOne')}</button>` : ''
|
|
394
|
-
const rollbackBtn = tool.canUpgrade && tool.npmPkg ? `<button class="btn btn-outline btn-sm" onclick="doRollbackTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('rollback')}</button>` : ''
|
|
395
|
-
actions = `<button class="btn btn-primary btn-sm" onclick="doLaunchTool('${tool.id}')">${t('open')}</button>
|
|
396
|
-
<button class="btn btn-outline btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reconfigure')}</button>
|
|
397
|
-
${upgradeBtn}${rollbackBtn}
|
|
398
|
-
<button class="btn btn-danger btn-sm" onclick="doResetTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reset')}</button>`
|
|
399
|
-
hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
|
|
400
1184
|
}
|
|
401
1185
|
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
<div class="tool-body">
|
|
412
|
-
<div class="tool-name">${esc(tool.name)}</div>
|
|
413
|
-
<div class="tool-meta">${statusBadges} ${hintLine}</div>
|
|
414
|
-
${hintText}${launchLine}
|
|
415
|
-
</div>
|
|
416
|
-
<div class="tool-actions">${actions}</div>
|
|
417
|
-
</div>`
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// ── Tool actions ─────────────────────────────────────────────────────────────
|
|
421
|
-
async function doConfigureTool(id, name) {
|
|
422
|
-
const status = await api('status')
|
|
423
|
-
if (!status.loggedIn) { alert(t('needLogin')); return }
|
|
424
|
-
if (busy) return
|
|
425
|
-
busy = true
|
|
426
|
-
openConsole(`${t('configure')}: ${name}`)
|
|
427
|
-
document.getElementById('console-section').classList.add('busy')
|
|
428
|
-
|
|
429
|
-
await streamSSE('/api/tool/configure', { toolId: id }, (ev) => {
|
|
430
|
-
if (ev.type === 'progress') appendLog(ev.message, 'info')
|
|
431
|
-
else if (ev.type === 'result') {
|
|
432
|
-
const cls = ev.status === 'ok' ? 'ok' : ev.status === 'warning' ? 'warn' : 'err'
|
|
433
|
-
appendLog(`${ev.status === 'ok' ? '✓' : '⚠'} ${ev.message}`, cls)
|
|
434
|
-
if (ev.file) appendLog(` → ${ev.file}`, 'info')
|
|
435
|
-
if (ev.hot) appendLog(` ${t('hotReload')}`, 'ok')
|
|
436
|
-
else if (ev.status === 'ok') appendLog(` ${t('needRestart')}`, 'warn')
|
|
437
|
-
if (ev.steps) ev.steps.forEach(s => appendLog(` · ${s}`, 'info'))
|
|
438
|
-
}
|
|
439
|
-
else if (ev.type === 'error') appendLog(`✗ ${ev.message}`, 'err')
|
|
440
|
-
else if (ev.type === 'done') {
|
|
441
|
-
appendLog(ev.success ? `\n✓ ${t('configSuccess')}` : `\n✗ ${t('configFailed')}`, ev.success ? 'ok' : 'err')
|
|
442
|
-
if (ev.dashboardUrl) {
|
|
443
|
-
appendLog(`\n→ ${ev.dashboardUrl}`, 'ok')
|
|
444
|
-
window.open(ev.dashboardUrl, '_blank')
|
|
445
|
-
}
|
|
1186
|
+
const toolSelect = document.getElementById('conversation-tool-select')
|
|
1187
|
+
if (toolSelect && state.currentConversation) {
|
|
1188
|
+
toolSelect.onchange = async (event) => {
|
|
1189
|
+
await api(`workspace/conversations/${state.currentConversation.id}`, {
|
|
1190
|
+
method: 'PATCH',
|
|
1191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1192
|
+
body: JSON.stringify({ toolId: event.target.value }),
|
|
1193
|
+
}).catch((error) => alert(error.message))
|
|
1194
|
+
await refreshWorkspace(true)
|
|
446
1195
|
}
|
|
447
|
-
}
|
|
1196
|
+
}
|
|
448
1197
|
|
|
449
|
-
document.getElementById('
|
|
450
|
-
|
|
451
|
-
loadTools()
|
|
1198
|
+
const list = document.getElementById('message-list')
|
|
1199
|
+
if (list) list.scrollTop = list.scrollHeight
|
|
452
1200
|
}
|
|
453
1201
|
|
|
454
|
-
async function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
1202
|
+
async function refreshWorkspace(keepConversation) {
|
|
1203
|
+
state.workspace = await api('workspace/state').catch(() => state.workspace)
|
|
1204
|
+
if (keepConversation && state.currentConversationId) {
|
|
1205
|
+
await loadConversation(state.currentConversationId)
|
|
1206
|
+
}
|
|
1207
|
+
render()
|
|
459
1208
|
}
|
|
460
1209
|
|
|
461
|
-
async function
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1210
|
+
async function createConversation() {
|
|
1211
|
+
const title = prompt('Conversation title', 'New Conversation')
|
|
1212
|
+
if (title == null) return
|
|
1213
|
+
const toolId = state.currentConversation?.toolId || 'codex'
|
|
1214
|
+
const payload = await api('workspace/conversations', {
|
|
1215
|
+
method: 'POST',
|
|
1216
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1217
|
+
body: JSON.stringify({ title, toolId }),
|
|
1218
|
+
})
|
|
1219
|
+
state.currentConversationId = payload.conversation.id
|
|
1220
|
+
await loadConversation(payload.conversation.id)
|
|
1221
|
+
await refreshWorkspace(true)
|
|
1222
|
+
setRoute('workspace')
|
|
466
1223
|
}
|
|
467
1224
|
|
|
468
|
-
async function
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1225
|
+
async function openConversation(id) {
|
|
1226
|
+
state.currentConversationId = id
|
|
1227
|
+
await loadConversation(id)
|
|
1228
|
+
if (state.route !== 'workspace') state.route = 'workspace'
|
|
1229
|
+
render()
|
|
472
1230
|
}
|
|
473
1231
|
|
|
474
|
-
async function
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1232
|
+
async function sendConversationMessage() {
|
|
1233
|
+
const input = document.getElementById('conversation-input')
|
|
1234
|
+
if (!input) return
|
|
1235
|
+
const content = input.value.trim()
|
|
1236
|
+
if (!content || !state.currentConversationId) return
|
|
1237
|
+
input.value = ''
|
|
1238
|
+
await api(`workspace/conversations/${state.currentConversationId}/messages`, {
|
|
1239
|
+
method: 'POST',
|
|
1240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1241
|
+
body: JSON.stringify({ content }),
|
|
1242
|
+
}).catch((error) => alert(error.message))
|
|
1243
|
+
await refreshWorkspace(true)
|
|
480
1244
|
}
|
|
481
1245
|
|
|
482
|
-
async function
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
1246
|
+
async function renameConversation() {
|
|
1247
|
+
if (!state.currentConversation) return
|
|
1248
|
+
const title = prompt('Rename conversation', state.currentConversation.title)
|
|
1249
|
+
if (title == null) return
|
|
1250
|
+
await api(`workspace/conversations/${state.currentConversation.id}`, {
|
|
1251
|
+
method: 'PATCH',
|
|
1252
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1253
|
+
body: JSON.stringify({ title }),
|
|
1254
|
+
}).catch((error) => alert(error.message))
|
|
1255
|
+
await refreshWorkspace(true)
|
|
1256
|
+
}
|
|
487
1257
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
})
|
|
1258
|
+
async function togglePinConversation() {
|
|
1259
|
+
if (!state.currentConversation) return
|
|
1260
|
+
await api(`workspace/conversations/${state.currentConversation.id}`, {
|
|
1261
|
+
method: 'PATCH',
|
|
1262
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1263
|
+
body: JSON.stringify({ pinned: !state.currentConversation.pinned }),
|
|
1264
|
+
}).catch((error) => alert(error.message))
|
|
1265
|
+
await refreshWorkspace(true)
|
|
1266
|
+
}
|
|
495
1267
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
async function doUpgrade() {
|
|
502
|
-
if (busy) return
|
|
503
|
-
busy = true
|
|
504
|
-
openConsole(t('upgrade'))
|
|
505
|
-
document.getElementById('console-section').classList.add('busy')
|
|
506
|
-
|
|
507
|
-
await streamSSE('/api/upgrade', {}, (ev) => {
|
|
508
|
-
if (ev.type === 'tool') {
|
|
509
|
-
const cls = ev.status === 'ok' ? 'ok' : ev.status === 'error' ? 'err' : ev.status === 'not-installed' ? 'warn' : 'info'
|
|
510
|
-
const msg = ev.status === 'not-installed' ? `${ev.name}: ${t('notInstalled')}`
|
|
511
|
-
: ev.status === 'upgrading' ? `${ev.name}: ${t('upgrade')}... (${ev.localVer || '?'})`
|
|
512
|
-
: ev.status === 'ok' ? `✓ ${ev.name}: ${ev.localVer || '?'} → ${ev.newVer || 'latest'}`
|
|
513
|
-
: `✗ ${ev.name}: ${t('configFailed')}`
|
|
514
|
-
appendLog(msg, cls)
|
|
515
|
-
} else if (ev.type === 'output') { appendLogRaw(ev.text) }
|
|
516
|
-
else if (ev.type === 'done') { appendLog(`\n✓ ${t('configSuccess')}`, 'ok') }
|
|
1268
|
+
function clearTaskForm() {
|
|
1269
|
+
;['task-title', 'task-prompt', 'task-schedule'].forEach((id) => {
|
|
1270
|
+
const field = document.getElementById(id)
|
|
1271
|
+
if (field) field.value = ''
|
|
517
1272
|
})
|
|
1273
|
+
const model = document.getElementById('task-model-override')
|
|
1274
|
+
if (model) model.value = ''
|
|
1275
|
+
delete window.__editingTaskId
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function saveTask() {
|
|
1279
|
+
const payload = {
|
|
1280
|
+
title: document.getElementById('task-title').value.trim(),
|
|
1281
|
+
prompt: document.getElementById('task-prompt').value.trim(),
|
|
1282
|
+
schedule: document.getElementById('task-schedule').value.trim(),
|
|
1283
|
+
modelOverride: document.getElementById('task-model-override').value,
|
|
1284
|
+
active: true,
|
|
1285
|
+
}
|
|
1286
|
+
if (window.__editingTaskId) payload.id = window.__editingTaskId
|
|
1287
|
+
const route = payload.id ? `workspace/tasks/${payload.id}` : 'workspace/tasks'
|
|
1288
|
+
const method = payload.id ? 'PATCH' : 'POST'
|
|
1289
|
+
await api(route, {
|
|
1290
|
+
method,
|
|
1291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1292
|
+
body: JSON.stringify(payload),
|
|
1293
|
+
}).catch((error) => alert(error.message))
|
|
1294
|
+
clearTaskForm()
|
|
1295
|
+
await refreshWorkspace(false)
|
|
1296
|
+
setRoute('tasks')
|
|
1297
|
+
}
|
|
518
1298
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1299
|
+
function editTask(id) {
|
|
1300
|
+
const task = (state.workspace?.scheduledTasks || []).find((item) => item.id === id)
|
|
1301
|
+
if (!task) return
|
|
1302
|
+
document.getElementById('task-title').value = task.title
|
|
1303
|
+
document.getElementById('task-prompt').value = task.prompt
|
|
1304
|
+
document.getElementById('task-schedule').value = task.schedule
|
|
1305
|
+
document.getElementById('task-model-override').value = task.modelOverride || ''
|
|
1306
|
+
window.__editingTaskId = task.id
|
|
522
1307
|
}
|
|
523
1308
|
|
|
524
|
-
async function
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1309
|
+
async function deleteTask(id) {
|
|
1310
|
+
if (!confirm('Delete this task?')) return
|
|
1311
|
+
await api(`workspace/tasks/${id}`, { method: 'DELETE' }).catch((error) => alert(error.message))
|
|
1312
|
+
await refreshWorkspace(false)
|
|
1313
|
+
setRoute('tasks')
|
|
1314
|
+
}
|
|
529
1315
|
|
|
530
|
-
|
|
531
|
-
await
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
upgraded = ev.success
|
|
536
|
-
appendLog(ev.success ? `\n✓ ${t('configSuccess')}` : `\n✗ ${t('configFailed')}`, ev.success ? 'ok' : 'err')
|
|
537
|
-
}
|
|
538
|
-
})
|
|
1316
|
+
async function runTask(id) {
|
|
1317
|
+
await api(`workspace/tasks/${id}`, { method: 'POST' }).catch((error) => alert(error.message))
|
|
1318
|
+
await refreshWorkspace(false)
|
|
1319
|
+
setRoute('tasks')
|
|
1320
|
+
}
|
|
539
1321
|
|
|
540
|
-
|
|
541
|
-
|
|
1322
|
+
async function saveRuntimeConfig() {
|
|
1323
|
+
const payload = {
|
|
1324
|
+
apiKey: document.getElementById('runtime-api-key').value.trim(),
|
|
1325
|
+
baseUrl: document.getElementById('runtime-base-url').value.trim(),
|
|
1326
|
+
model: document.getElementById('runtime-model').value,
|
|
1327
|
+
}
|
|
1328
|
+
await api('workspace/api-config', {
|
|
1329
|
+
method: 'POST',
|
|
1330
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1331
|
+
body: JSON.stringify(payload),
|
|
1332
|
+
}).catch((error) => alert(error.message))
|
|
1333
|
+
await refreshWorkspace(false)
|
|
1334
|
+
setRoute('tasks')
|
|
1335
|
+
}
|
|
542
1336
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
setTimeout(() => location.reload(), 4000)
|
|
1337
|
+
function copyLoginKey() {
|
|
1338
|
+
const key = state.workspace?.holySheepApi?.apiKey
|
|
1339
|
+
if (!key) {
|
|
1340
|
+
alert('Login first so the workspace can seed the HolySheep API config.')
|
|
548
1341
|
return
|
|
549
1342
|
}
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
async function doRollbackTool(id, name) {
|
|
554
|
-
if (busy) return
|
|
555
|
-
if (!confirm(lang === 'zh' ? `确认回退 ${name} 到上一个版本?` : `Rollback ${name} to previous version?`)) return
|
|
556
|
-
busy = true
|
|
557
|
-
openConsole(`${t('rollback')}: ${name}`)
|
|
558
|
-
document.getElementById('console-section').classList.add('busy')
|
|
559
|
-
|
|
560
|
-
let success = false
|
|
561
|
-
await streamSSE('/api/tool/rollback', { toolId: id }, (ev) => {
|
|
562
|
-
if (ev.type === 'progress') appendLog(ev.message, 'info')
|
|
563
|
-
else if (ev.type === 'output') appendLogRaw(ev.text)
|
|
564
|
-
else if (ev.type === 'done') {
|
|
565
|
-
success = ev.success
|
|
566
|
-
appendLog(ev.success ? `\n✓ ${lang === 'zh' ? '回退成功' : 'Rollback succeeded'}` : `\n✗ ${lang === 'zh' ? '回退失败' : 'Rollback failed'}`, ev.success ? 'ok' : 'err')
|
|
567
|
-
}
|
|
568
|
-
})
|
|
1343
|
+
document.getElementById('runtime-api-key').value = key
|
|
1344
|
+
}
|
|
569
1345
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1346
|
+
async function launchTool(toolId) {
|
|
1347
|
+
const result = await api('tool/launch', {
|
|
1348
|
+
method: 'POST',
|
|
1349
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1350
|
+
body: JSON.stringify({ toolId }),
|
|
1351
|
+
}).catch((error) => alert(error.message))
|
|
1352
|
+
if (result?.url) alert(`Opened: ${result.url}`)
|
|
573
1353
|
}
|
|
574
1354
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
document.getElementById('console-title').textContent = title || t('log')
|
|
579
|
-
document.getElementById('console-output').innerHTML = ''
|
|
580
|
-
area.classList.add('open')
|
|
581
|
-
area.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
1355
|
+
async function launchCurrentTool() {
|
|
1356
|
+
if (!state.currentConversation?.toolId) return
|
|
1357
|
+
return launchTool(state.currentConversation.toolId)
|
|
582
1358
|
}
|
|
583
1359
|
|
|
584
|
-
function
|
|
585
|
-
document.getElementById('console-
|
|
1360
|
+
function openConsole(title) {
|
|
1361
|
+
document.getElementById('console-title').textContent = title
|
|
1362
|
+
document.getElementById('console-log').textContent = ''
|
|
1363
|
+
document.getElementById('console-drawer').classList.add('open')
|
|
586
1364
|
}
|
|
587
1365
|
|
|
588
|
-
function
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
1366
|
+
function appendConsole(text, klass) {
|
|
1367
|
+
const line = document.createElement('div')
|
|
1368
|
+
if (klass) line.className = klass
|
|
1369
|
+
line.textContent = text
|
|
1370
|
+
const log = document.getElementById('console-log')
|
|
1371
|
+
log.appendChild(line)
|
|
1372
|
+
log.scrollTop = log.scrollHeight
|
|
592
1373
|
}
|
|
593
1374
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
el.innerHTML += esc(text)
|
|
597
|
-
el.scrollTop = el.scrollHeight
|
|
1375
|
+
document.getElementById('console-close').onclick = () => {
|
|
1376
|
+
document.getElementById('console-drawer').classList.remove('open')
|
|
598
1377
|
}
|
|
599
1378
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
1379
|
+
async function consumeSse(path, body, title) {
|
|
1380
|
+
openConsole(title)
|
|
1381
|
+
const response = await fetch(`/api/${path}`, {
|
|
603
1382
|
method: 'POST',
|
|
604
1383
|
headers: { 'Content-Type': 'application/json' },
|
|
605
1384
|
body: JSON.stringify(body),
|
|
606
1385
|
})
|
|
607
|
-
const reader =
|
|
1386
|
+
const reader = response.body.getReader()
|
|
608
1387
|
const decoder = new TextDecoder()
|
|
609
1388
|
let buffer = ''
|
|
610
1389
|
while (true) {
|
|
611
|
-
const {
|
|
1390
|
+
const { value, done } = await reader.read()
|
|
612
1391
|
if (done) break
|
|
613
1392
|
buffer += decoder.decode(value, { stream: true })
|
|
614
|
-
const
|
|
615
|
-
buffer =
|
|
616
|
-
for (const
|
|
617
|
-
const line =
|
|
1393
|
+
const chunks = buffer.split('\n\n')
|
|
1394
|
+
buffer = chunks.pop()
|
|
1395
|
+
for (const chunk of chunks) {
|
|
1396
|
+
const line = chunk.split('\n').find((entry) => entry.startsWith('data:'))
|
|
618
1397
|
if (!line) continue
|
|
619
|
-
|
|
1398
|
+
const payload = JSON.parse(line.slice(5).trim())
|
|
1399
|
+
if (payload.type === 'output') appendConsole(payload.text, '')
|
|
1400
|
+
else if (payload.type === 'progress') appendConsole(payload.message || JSON.stringify(payload), payload.status === 'error' ? 'error' : payload.status === 'ok' ? 'ok' : 'warn')
|
|
1401
|
+
else if (payload.type === 'done') appendConsole('Done.', payload.success === false ? 'error' : 'ok')
|
|
1402
|
+
else appendConsole(JSON.stringify(payload), '')
|
|
620
1403
|
}
|
|
621
1404
|
}
|
|
622
1405
|
}
|
|
623
1406
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
el.innerHTML = `<div class="section-title">${t('env')}</div><div class="card"><span class="loading">${t('checking')}</span></div>`
|
|
628
|
-
|
|
629
|
-
const data = await api('env')
|
|
630
|
-
const vars = data.vars || {}
|
|
631
|
-
const rcFiles = data.rcFiles || []
|
|
632
|
-
|
|
633
|
-
let rows = ''
|
|
634
|
-
for (const [k, v] of Object.entries(vars)) {
|
|
635
|
-
const isSet = v !== null
|
|
636
|
-
rows += `<div class="env-row">
|
|
637
|
-
<span class="dot ${isSet ? 'dot-ok' : 'dot-gray'}" style="width:8px;height:8px"></span>
|
|
638
|
-
<span class="env-key">${k}</span>
|
|
639
|
-
<span class="env-val" style="color:var(${isSet ? '--success' : '--text2'})">${isSet ? t('set') : t('notSet')}</span>
|
|
640
|
-
</div>`
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
let rcInfo = ''
|
|
644
|
-
if (rcFiles.length) {
|
|
645
|
-
rcInfo = `<div class="env-rc">${t('shellConfig')}: ${rcFiles.map(f =>
|
|
646
|
-
`<span class="mono">${esc(f.path)}</span> (${f.hasManagedBlock ? t('managedBlock') : t('noManagedBlock')})`
|
|
647
|
-
).join(', ')}</div>`
|
|
648
|
-
}
|
|
1407
|
+
function configureTool(toolId) {
|
|
1408
|
+
consumeSse('tool/configure', { toolId }, `Configure ${toolNames[toolId] || toolId}`).then(() => refreshWorkspace(false))
|
|
1409
|
+
}
|
|
649
1410
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
async function doCleanEnv(btn) {
|
|
661
|
-
btn.disabled = true
|
|
662
|
-
btn.textContent = t('cleaning')
|
|
663
|
-
try {
|
|
664
|
-
const r = await api('env/clean', { method: 'POST' })
|
|
665
|
-
btn.textContent = `✓ ${t('cleanDone')}`
|
|
666
|
-
setTimeout(() => loadEnv(), 1500)
|
|
667
|
-
} catch (e) {
|
|
668
|
-
btn.textContent = e.message
|
|
669
|
-
}
|
|
1411
|
+
async function login() {
|
|
1412
|
+
const apiKey = document.getElementById('login-api-key').value.trim()
|
|
1413
|
+
await api('login', {
|
|
1414
|
+
method: 'POST',
|
|
1415
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1416
|
+
body: JSON.stringify({ apiKey }),
|
|
1417
|
+
}).catch((error) => alert(error.message))
|
|
1418
|
+
await loadBaseState()
|
|
1419
|
+
render()
|
|
670
1420
|
}
|
|
671
1421
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
<div class="footer-links">
|
|
677
|
-
<a href="https://holysheep.ai" target="_blank">${t('docs')}</a>
|
|
678
|
-
<a href="https://holysheep.ai/app/recharge" target="_blank">${t('recharge')}</a>
|
|
679
|
-
<a href="https://holysheep.ai/register" target="_blank">${lang === 'zh' ? '注册' : 'Register'}</a>
|
|
680
|
-
<a href="https://holysheep.ai/pricing" target="_blank">${t('pricing')}</a>
|
|
681
|
-
</div>
|
|
682
|
-
<div class="footer-sub">${t('slogan')}</div>
|
|
683
|
-
</div>`
|
|
1422
|
+
async function logout() {
|
|
1423
|
+
await api('logout', { method: 'POST' }).catch((error) => alert(error.message))
|
|
1424
|
+
await loadBaseState()
|
|
1425
|
+
render()
|
|
684
1426
|
}
|
|
685
1427
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const d = document.createElement('div')
|
|
690
|
-
d.textContent = String(s)
|
|
691
|
-
return d.innerHTML
|
|
1428
|
+
document.getElementById('refresh-btn').onclick = async () => {
|
|
1429
|
+
await loadBaseState()
|
|
1430
|
+
render()
|
|
692
1431
|
}
|
|
693
1432
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1433
|
+
window.addEventListener('hashchange', () => {
|
|
1434
|
+
state.route = routeFromHash()
|
|
1435
|
+
render()
|
|
1436
|
+
})
|
|
1437
|
+
|
|
1438
|
+
async function bootstrap() {
|
|
1439
|
+
state.route = routeFromHash()
|
|
1440
|
+
await loadBaseState()
|
|
1441
|
+
render()
|
|
699
1442
|
}
|
|
1443
|
+
|
|
1444
|
+
bootstrap()
|
|
700
1445
|
</script>
|
|
701
1446
|
</body>
|
|
702
1447
|
</html>
|