@wendongfly/myhi 1.3.175 → 1.3.180
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/dist/admin.html +498 -497
- package/dist/chat.html +273 -4
- package/dist/grid.html +448 -447
- package/dist/index.html +88 -2
- package/dist/index.js +4 -4
- package/dist/index.min.js +138 -138
- package/dist/lib/machine.js +50 -0
- package/dist/terminal.html +1064 -1063
- package/package.json +1 -1
package/dist/grid.html
CHANGED
|
@@ -1,447 +1,448 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
-
<meta name="theme-color" content="#0d1117">
|
|
7
|
-
<title>myhi
|
|
8
|
-
<script src="/
|
|
9
|
-
<script>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
r.setAttribute('data-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
body {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.bar-btn
|
|
39
|
-
.
|
|
40
|
-
.seg
|
|
41
|
-
.seg button
|
|
42
|
-
.seg button
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.pane
|
|
47
|
-
.pane-head
|
|
48
|
-
.pane-head
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.pane-dot
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.pane-head button
|
|
58
|
-
.pane-
|
|
59
|
-
.pane-body
|
|
60
|
-
.pane-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
</
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
<button data-lo="
|
|
71
|
-
<button data-lo="
|
|
72
|
-
<button data-lo="
|
|
73
|
-
<button data-lo="
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
<button class="bar-btn" onclick="
|
|
78
|
-
<button class="bar-btn" onclick="
|
|
79
|
-
<button class="bar-btn"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div id="
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<div style="position:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<div style="position:
|
|
106
|
-
|
|
107
|
-
<div
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
<
|
|
114
|
-
<
|
|
115
|
-
<div style="display:flex;gap:0.
|
|
116
|
-
|
|
117
|
-
<button onclick="
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return { layout: 'auto', panes:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
function
|
|
141
|
-
function
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
for (const s of sessions) if (key.
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
else if (lo === '
|
|
161
|
-
else cols = 'repeat(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
pane.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
files.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
pane.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
pane.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
ifr
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let
|
|
245
|
-
let
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (diff <
|
|
253
|
-
if (diff <
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
document.getElementById('
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
function
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
tagbar.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
items.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
<div style="font-size:0.
|
|
300
|
-
${
|
|
301
|
-
${
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
${s.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
{ icon:'
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
function
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
document.getElementById('
|
|
364
|
-
document.getElementById('nt-
|
|
365
|
-
document.getElementById('nt-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
window.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
function
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
pane._dot.
|
|
422
|
-
pane._dot.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
</
|
|
447
|
-
</
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#0d1117">
|
|
7
|
+
<title>myhi</title>
|
|
8
|
+
<script src="/lib/machine.js"></script>
|
|
9
|
+
<script src="/sio-client/socket.io.min.js"></script>
|
|
10
|
+
<script>
|
|
11
|
+
(function(){try{
|
|
12
|
+
var t=localStorage.getItem('myhi_theme')||'dark';
|
|
13
|
+
var f=localStorage.getItem('myhi_font')||'medium';
|
|
14
|
+
var r=document.documentElement;
|
|
15
|
+
if(t&&t!=='dark') r.setAttribute('data-theme',t);
|
|
16
|
+
r.setAttribute('data-font',f);
|
|
17
|
+
}catch(e){}})();
|
|
18
|
+
</script>
|
|
19
|
+
<style>
|
|
20
|
+
:root {
|
|
21
|
+
--bg:#0d1117; --surface:#161b22; --surface2:#21262d; --border:#30363d;
|
|
22
|
+
--text:#e6edf3; --text2:#c9d1d9; --muted:#8b949e; --dim:#6e7681;
|
|
23
|
+
--blue:#58a6ff; --blue2:#1f6feb; --green:#3fb950; --red:#f85149; --purple:#7c3aed;
|
|
24
|
+
}
|
|
25
|
+
:root[data-theme="light"] { --bg:#ffffff; --surface:#f6f8fa; --surface2:#eaeef2; --border:#d0d7de;
|
|
26
|
+
--text:#1f2328; --text2:#32383f; --muted:#656d76; --dim:#818b98;
|
|
27
|
+
--blue:#0969da; --blue2:#0969da; --green:#1a7f37; --red:#cf222e; --purple:#8250df; }
|
|
28
|
+
:root[data-theme="black"] { --bg:#000000; --surface:#0a0a0a; --surface2:#161616; --border:#262626; }
|
|
29
|
+
@media (prefers-color-scheme: light) { :root[data-theme="system"] { --bg:#ffffff; --surface:#f6f8fa; --surface2:#eaeef2; --border:#d0d7de;
|
|
30
|
+
--text:#1f2328; --text2:#32383f; --muted:#656d76; --dim:#818b98;
|
|
31
|
+
--blue:#0969da; --blue2:#0969da; --green:#1a7f37; --red:#cf222e; --purple:#8250df; } }
|
|
32
|
+
html { font-size:16px; } html[data-font="small"]{font-size:15px;} html[data-font="large"]{font-size:17px;}
|
|
33
|
+
* { box-sizing:border-box; }
|
|
34
|
+
html,body { margin:0; height:100%; background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }
|
|
35
|
+
body { display:flex; flex-direction:column; overflow:hidden; }
|
|
36
|
+
#bar { flex-shrink:0; display:flex; align-items:center; gap:0.5rem; padding:0.4rem 0.6rem; background:var(--surface); border-bottom:1px solid var(--border); flex-wrap:wrap; }
|
|
37
|
+
.logo { font-weight:700; font-size:0.95rem; color:var(--text); } .logo span{ color:var(--blue); }
|
|
38
|
+
.bar-btn { background:var(--bg); border:1px solid var(--border); color:var(--text2); font-size:0.78rem; padding:0.32rem 0.6rem; border-radius:7px; cursor:pointer; white-space:nowrap; }
|
|
39
|
+
.bar-btn:hover { border-color:var(--blue2); }
|
|
40
|
+
.seg { display:inline-flex; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
|
|
41
|
+
.seg button { background:var(--bg); border:none; border-left:1px solid var(--border); color:var(--muted); font-size:0.76rem; padding:0.32rem 0.6rem; cursor:pointer; white-space:nowrap; }
|
|
42
|
+
.seg button:first-child { border-left:none; }
|
|
43
|
+
.seg button.active { background:var(--purple); color:#fff; }
|
|
44
|
+
.spacer { flex:1; }
|
|
45
|
+
#grid { flex:1; min-height:0; display:grid; gap:6px; padding:6px; overflow:auto; }
|
|
46
|
+
.pane { display:flex; flex-direction:column; min-height:260px; min-width:0; background:var(--surface); border:1px solid var(--border); border-radius:10px; overflow:hidden; }
|
|
47
|
+
.pane-head { flex-shrink:0; display:flex; align-items:center; gap:0.4rem; padding:0.3rem 0.4rem; background:var(--surface2); border-bottom:1px solid var(--border); }
|
|
48
|
+
.pane-head select { flex:1; min-width:0; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:0.28rem 0.4rem; font-size:0.78rem; }
|
|
49
|
+
.pane-head .pane-picker { flex:1; min-width:0; display:flex; align-items:center; justify-content:space-between; gap:0.3rem; text-align:left; }
|
|
50
|
+
#tp-list [data-id]:hover { background:var(--surface2) !important; }
|
|
51
|
+
/* 窗格状态灯 */
|
|
52
|
+
.pane-dot { width:9px; height:9px; border-radius:50%; background:var(--dim); flex-shrink:0; }
|
|
53
|
+
.pane-dot.pulse { animation:panepulse 1.1s ease-in-out infinite; }
|
|
54
|
+
@keyframes panepulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
55
|
+
.pane-head.flash { animation:paneflash 1s ease-in-out 3; }
|
|
56
|
+
@keyframes paneflash { 0%,100%{background:var(--surface2)} 50%{background:var(--blue2)} }
|
|
57
|
+
.pane-head button { background:var(--bg); border:1px solid var(--border); color:var(--muted); border-radius:6px; padding:0.2rem 0.45rem; font-size:0.8rem; cursor:pointer; line-height:1; }
|
|
58
|
+
.pane-head button:hover { color:var(--text); border-color:var(--blue2); }
|
|
59
|
+
.pane-body { flex:1; min-height:0; position:relative; }
|
|
60
|
+
.pane-body iframe { width:100%; height:100%; border:none; display:block; background:var(--bg); }
|
|
61
|
+
.pane-empty { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:var(--dim); font-size:0.82rem; text-align:center; padding:1rem; }
|
|
62
|
+
#empty-hint { flex:1; display:flex; align-items:center; justify-content:center; color:var(--dim); font-size:0.9rem; text-align:center; padding:2rem; }
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div id="bar">
|
|
67
|
+
<div class="logo">my<span>hi</span> 分屏</div>
|
|
68
|
+
<button class="bar-btn" onclick="location.href='/'">← 首页</button>
|
|
69
|
+
<div class="seg" id="seg-layout">
|
|
70
|
+
<button data-lo="auto">自动</button>
|
|
71
|
+
<button data-lo="1">单屏</button>
|
|
72
|
+
<button data-lo="2">左右2</button>
|
|
73
|
+
<button data-lo="4">田字4</button>
|
|
74
|
+
<button data-lo="3">3列</button>
|
|
75
|
+
</div>
|
|
76
|
+
<span class="spacer"></span>
|
|
77
|
+
<button class="bar-btn" id="notify-btn" onclick="toggleNotify()" title="任务完成/待回答时桌面通知+提示音(默认关)">🔔 关</button>
|
|
78
|
+
<button class="bar-btn" onclick="refreshSessions(true)" title="刷新任务列表">↻</button>
|
|
79
|
+
<button class="bar-btn" onclick="addPane('')" title="加一个空窗格,从下拉选已有任务">+ 窗格</button>
|
|
80
|
+
<button class="bar-btn" style="border-color:var(--green);color:var(--green)" onclick="openNewTask()" title="新建任务(和首页一致:预设/名称/命令/目录)">🆕 新建任务</button>
|
|
81
|
+
</div>
|
|
82
|
+
<div id="grid"></div>
|
|
83
|
+
<div id="empty-hint" style="display:none">点「+ 窗格」或「🆕 新建任务」把多个任务并排看。<br>拉宽窗口自动多几格,或用上方按钮锁定布局。</div>
|
|
84
|
+
|
|
85
|
+
<!-- 任务选择弹窗(搜索 + 分组 + 标签)-->
|
|
86
|
+
<div id="task-picker" style="display:none;position:fixed;inset:0;z-index:100">
|
|
87
|
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.5)" onclick="closeTaskPicker()"></div>
|
|
88
|
+
<div style="position:relative;max-width:540px;width:92%;margin:6vh auto 0;background:var(--surface);border:1px solid var(--border);border-radius:12px;max-height:82vh;display:flex;flex-direction:column;box-shadow:0 16px 48px rgba(0,0,0,0.45)">
|
|
89
|
+
<div style="padding:0.6rem 0.8rem;border-bottom:1px solid var(--border);display:flex;gap:0.5rem;align-items:center">
|
|
90
|
+
<input id="tp-search" placeholder="搜索任务(名称 / 目录 / 标签)…" autocomplete="off" style="flex:1;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.42rem 0.6rem;font-size:0.82rem">
|
|
91
|
+
<button onclick="closeTaskPicker()" style="background:var(--bg);border:1px solid var(--border);color:var(--muted);border-radius:7px;padding:0.38rem 0.6rem;cursor:pointer">✕</button>
|
|
92
|
+
</div>
|
|
93
|
+
<div style="padding:0.45rem 0.8rem;display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;border-bottom:1px solid var(--border)">
|
|
94
|
+
<div class="seg" id="tp-group">
|
|
95
|
+
<button data-g="time">最近</button><button data-g="project">项目</button><button data-g="tag">标签</button>
|
|
96
|
+
</div>
|
|
97
|
+
<span id="tp-tagbar" style="display:none;gap:0.3rem;flex-wrap:wrap;align-items:center"></span>
|
|
98
|
+
</div>
|
|
99
|
+
<div id="tp-list" style="flex:1;overflow-y:auto;padding:0.4rem 0.6rem"></div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- 新建任务面板(和首页「新建终端会话」一致)-->
|
|
104
|
+
<div id="new-task" style="display:none;position:fixed;inset:0;z-index:100">
|
|
105
|
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.5)" onclick="closeNewTask()"></div>
|
|
106
|
+
<div style="position:relative;max-width:460px;width:92%;margin:8vh auto 0;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:1rem;box-shadow:0 16px 48px rgba(0,0,0,0.45)">
|
|
107
|
+
<div style="font-weight:600;margin-bottom:0.7rem">新建任务</div>
|
|
108
|
+
<div id="nt-presets" style="display:flex;gap:0.4rem;margin-bottom:0.7rem;flex-wrap:wrap"></div>
|
|
109
|
+
<label style="font-size:0.72rem;color:var(--muted)">名称 <span style="color:var(--red)">必填</span></label>
|
|
110
|
+
<input id="nt-title" placeholder="请输入任务名称" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.6rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.85rem">
|
|
111
|
+
<label style="font-size:0.72rem;color:var(--muted)">启动命令(可选,终端类型用)</label>
|
|
112
|
+
<input id="nt-cmd" placeholder="留空仅打开 shell" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.6rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.82rem;font-family:monospace">
|
|
113
|
+
<label style="font-size:0.72rem;color:var(--muted)">工作目录(可选)</label>
|
|
114
|
+
<input id="nt-cwd" placeholder="留空 → 建在 默认目录/任务名 下(每个任务独立目录)" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.4rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.82rem">
|
|
115
|
+
<div id="nt-recent" style="display:flex;flex-wrap:wrap;gap:0.3rem;margin-bottom:0.7rem"></div>
|
|
116
|
+
<div style="display:flex;gap:0.5rem">
|
|
117
|
+
<button onclick="closeNewTask()" style="flex:1;background:var(--bg);border:1px solid var(--border);color:var(--muted);border-radius:8px;padding:0.5rem;cursor:pointer">取消</button>
|
|
118
|
+
<button onclick="submitNewTask()" style="flex:2;background:var(--green);border:none;color:#fff;border-radius:8px;padding:0.5rem;cursor:pointer;font-weight:600">创建</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script>
|
|
124
|
+
const LS_KEY = 'myhi_grid_v1';
|
|
125
|
+
const gridEl = document.getElementById('grid');
|
|
126
|
+
const emptyHint = document.getElementById('empty-hint');
|
|
127
|
+
let sessions = []; // [{id,title,cwd}]
|
|
128
|
+
let state = loadState(); // { layout, panes:[sessionId,...] }
|
|
129
|
+
const paneEls = []; // 与可见窗格一一对应的 DOM(保序,保留 iframe 不重载)
|
|
130
|
+
|
|
131
|
+
function loadState() {
|
|
132
|
+
try { const s = JSON.parse(localStorage.getItem(LS_KEY)); if (s && Array.isArray(s.panes)) return { layout: s.layout || 'auto', panes: s.panes }; } catch {}
|
|
133
|
+
return { layout: 'auto', panes: [] };
|
|
134
|
+
}
|
|
135
|
+
function saveState() {
|
|
136
|
+
// 存【稳定标识】而不是易变 id——myhi 会话 id 每次重启都重新 randomUUID,存 id 重启后必失效
|
|
137
|
+
state.panes = paneEls.map(p => p._key || null);
|
|
138
|
+
localStorage.setItem(LS_KEY, JSON.stringify(state));
|
|
139
|
+
}
|
|
140
|
+
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
141
|
+
function sName(s){ return s.title || s.name || s.summary || (s.id||s.sessionId||'').slice(0,12) || '未命名'; }
|
|
142
|
+
function sId(s){ return s.id || s.sessionId; }
|
|
143
|
+
function sessById(id){ return sessions.find(s => sId(s) === id); }
|
|
144
|
+
// 稳定标识:claudeSessionId(agent 会话,--resume 用,重启不变) / createdAt(持久化,稳定)
|
|
145
|
+
function keyOf(s){ return s ? { cid: s.claudeSessionId || null, createdAt: s.createdAt || null, cwd: s.cwd || null } : null; }
|
|
146
|
+
// 把稳定标识映射回【当前】会话 id(重启后 id 变了也能找回同一个会话);找不到返回 ''
|
|
147
|
+
function resolveKey(key){
|
|
148
|
+
if (!key) return '';
|
|
149
|
+
if (typeof key === 'string') return sessById(key) ? key : ''; // 兼容旧格式(曾直接存 id)
|
|
150
|
+
for (const s of sessions) if (key.cid && s.claudeSessionId && s.claudeSessionId === key.cid) return sId(s);
|
|
151
|
+
for (const s of sessions) if (key.createdAt && s.createdAt === key.createdAt && (!key.cwd || s.cwd === key.cwd)) return sId(s);
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── 布局 ──
|
|
156
|
+
function applyLayout() {
|
|
157
|
+
const lo = state.layout;
|
|
158
|
+
let cols;
|
|
159
|
+
if (lo === 'auto') cols = 'repeat(auto-fit, minmax(min(100%, 460px), 1fr))';
|
|
160
|
+
else if (lo === '1') cols = '1fr';
|
|
161
|
+
else if (lo === '3') cols = 'repeat(3, 1fr)';
|
|
162
|
+
else cols = 'repeat(2, 1fr)'; // '2' 左右 / '4' 田字 都是 2 列,行数由窗格数决定
|
|
163
|
+
gridEl.style.gridTemplateColumns = cols;
|
|
164
|
+
document.querySelectorAll('#seg-layout button').forEach(b => b.classList.toggle('active', b.dataset.lo === lo));
|
|
165
|
+
emptyHint.style.display = paneEls.length ? 'none' : '';
|
|
166
|
+
gridEl.style.display = paneEls.length ? 'grid' : 'none';
|
|
167
|
+
}
|
|
168
|
+
document.querySelectorAll('#seg-layout button').forEach(b => b.addEventListener('click', () => {
|
|
169
|
+
state.layout = b.dataset.lo;
|
|
170
|
+
// 田字:不足 4 格时自动补足空格,凑成 2×2 观感
|
|
171
|
+
if (b.dataset.lo === '4') { while (paneEls.length < 4) addPane('', true); }
|
|
172
|
+
saveState(); applyLayout();
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
// ── 窗格 ──
|
|
176
|
+
function updatePickerLabel(pane) {
|
|
177
|
+
const s = pane._sid ? sessById(pane._sid) : null;
|
|
178
|
+
pane._picker.innerHTML = s
|
|
179
|
+
? `<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(sName(s))}</span><span style="color:var(--dim);flex-shrink:0">▾</span>`
|
|
180
|
+
: `<span style="color:var(--muted)">+ 选择任务</span><span style="color:var(--dim);flex-shrink:0">▾</span>`;
|
|
181
|
+
}
|
|
182
|
+
function addPane(sid, skipSave) {
|
|
183
|
+
const pane = document.createElement('div'); pane.className = 'pane'; pane._sid = sid || '';
|
|
184
|
+
const head = document.createElement('div'); head.className = 'pane-head';
|
|
185
|
+
const dot = document.createElement('span'); dot.className = 'pane-dot'; dot.title = '任务状态';
|
|
186
|
+
const picker = document.createElement('button'); picker.className = 'pane-picker';
|
|
187
|
+
const files = document.createElement('button'); files.textContent='📁'; files.title='查看该任务的文件(在本格内浏览/下载,远程也能用)';
|
|
188
|
+
files.addEventListener('click', () => {
|
|
189
|
+
if (pane._iframe && pane._iframe.contentWindow) pane._iframe.contentWindow.postMessage({ type:'myhi-cmd', cmd:'open-files' }, '*');
|
|
190
|
+
else alert('先给这个格子选一个任务');
|
|
191
|
+
});
|
|
192
|
+
const full = document.createElement('button'); full.textContent='↗'; full.title='在新标签页打开';
|
|
193
|
+
const close = document.createElement('button'); close.textContent='✕'; close.title='关闭此格'; close.style.color='var(--red)';
|
|
194
|
+
head.appendChild(dot); head.appendChild(picker); head.appendChild(files); head.appendChild(full); head.appendChild(close);
|
|
195
|
+
pane._dot = dot; pane._head = head;
|
|
196
|
+
const body = document.createElement('div'); body.className = 'pane-body';
|
|
197
|
+
const empty = document.createElement('div'); empty.className='pane-empty'; empty.textContent='点上方「选择任务」挑一个(可搜索/分组/按标签)'; body.appendChild(empty);
|
|
198
|
+
pane.appendChild(head); pane.appendChild(body);
|
|
199
|
+
pane._picker = picker; pane._body = body; pane._empty = empty; pane._iframe = null;
|
|
200
|
+
|
|
201
|
+
picker.addEventListener('click', () => openTaskPicker(pane));
|
|
202
|
+
full.addEventListener('click', () => { if (pane._sid) window.open('/terminal/' + pane._sid, '_blank'); });
|
|
203
|
+
close.addEventListener('click', () => removePane(pane));
|
|
204
|
+
|
|
205
|
+
gridEl.appendChild(pane);
|
|
206
|
+
paneEls.push(pane);
|
|
207
|
+
updatePickerLabel(pane);
|
|
208
|
+
if (sid) setPaneSession(pane, sid);
|
|
209
|
+
if (!skipSave) { saveState(); }
|
|
210
|
+
applyLayout();
|
|
211
|
+
}
|
|
212
|
+
function setPaneSession(pane, sid) {
|
|
213
|
+
pane._sid = sid || '';
|
|
214
|
+
pane._key = sid ? keyOf(sessById(sid)) : null; // 记住稳定标识,供重启后找回
|
|
215
|
+
updatePickerLabel(pane);
|
|
216
|
+
// 只重建这一格的 iframe,其它格不受影响
|
|
217
|
+
if (pane._iframe) { pane._iframe.remove(); pane._iframe = null; }
|
|
218
|
+
if (sid) {
|
|
219
|
+
pane._empty.style.display = 'none';
|
|
220
|
+
const ifr = document.createElement('iframe');
|
|
221
|
+
ifr.src = '/terminal/' + encodeURIComponent(sid) + '?embed=1';
|
|
222
|
+
pane._body.appendChild(ifr); pane._iframe = ifr;
|
|
223
|
+
} else {
|
|
224
|
+
pane._empty.style.display = '';
|
|
225
|
+
}
|
|
226
|
+
saveState();
|
|
227
|
+
}
|
|
228
|
+
function removePane(pane) {
|
|
229
|
+
const i = paneEls.indexOf(pane); if (i >= 0) paneEls.splice(i, 1);
|
|
230
|
+
pane.remove(); saveState(); applyLayout();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── 任务列表 ──
|
|
234
|
+
async function refreshSessions(rebuild) {
|
|
235
|
+
try {
|
|
236
|
+
const r = await fetch('/api/sessions'); if (!r.ok) return;
|
|
237
|
+
const data = await r.json();
|
|
238
|
+
sessions = (Array.isArray(data) ? data : (data.sessions || [])).filter(s => sId(s));
|
|
239
|
+
} catch {}
|
|
240
|
+
if (rebuild) for (const pane of paneEls) updatePickerLabel(pane);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── 任务选择弹窗(搜索 + 最近/项目/标签分组 + 标签过滤,借鉴首页)──
|
|
244
|
+
let _pickTarget = null;
|
|
245
|
+
let _tpGroup = localStorage.getItem('myhi_grid_group') || 'time';
|
|
246
|
+
let _tpTag = null;
|
|
247
|
+
function sessionTime(s){ return s.lastActivityAt || s.createdAt || 0; }
|
|
248
|
+
// 与首页卡片同款相对时间(1.3.173:任务选择条目信息量对齐首页)
|
|
249
|
+
function fmtRelTime(ms) {
|
|
250
|
+
if (!ms) return '';
|
|
251
|
+
const diff = Date.now() - ms;
|
|
252
|
+
if (diff < 60000) return '刚刚';
|
|
253
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
|
|
254
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
|
|
255
|
+
const d = new Date(ms);
|
|
256
|
+
const hm = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
257
|
+
if (diff < 172800000) return '昨天 ' + hm;
|
|
258
|
+
return (d.getMonth() + 1) + '月' + d.getDate() + '日';
|
|
259
|
+
}
|
|
260
|
+
// 待发草稿数(角标),打开弹窗时拉一次
|
|
261
|
+
let _draftCounts = {};
|
|
262
|
+
function refreshDraftCounts() {
|
|
263
|
+
return fetch('/api/inbox').then(r => r.json()).then(d => { _draftCounts = d.counts || {}; }).catch(() => {});
|
|
264
|
+
}
|
|
265
|
+
window.openTaskPicker = function(pane) {
|
|
266
|
+
_pickTarget = pane;
|
|
267
|
+
document.getElementById('task-picker').style.display = '';
|
|
268
|
+
document.getElementById('tp-search').value = '';
|
|
269
|
+
refreshDraftCounts();
|
|
270
|
+
refreshSessions(false).then(renderPicker);
|
|
271
|
+
renderPicker();
|
|
272
|
+
setTimeout(() => document.getElementById('tp-search').focus(), 30);
|
|
273
|
+
};
|
|
274
|
+
window.closeTaskPicker = function(){ document.getElementById('task-picker').style.display='none'; _pickTarget=null; };
|
|
275
|
+
function pushMap(m,k,v){ if(!m.has(k)) m.set(k,[]); m.get(k).push(v); }
|
|
276
|
+
function renderPicker() {
|
|
277
|
+
document.querySelectorAll('#tp-group button').forEach(b => b.classList.toggle('active', b.dataset.g === _tpGroup));
|
|
278
|
+
const tagbar = document.getElementById('tp-tagbar');
|
|
279
|
+
if (_tpGroup === 'tag') {
|
|
280
|
+
const allTags = [...new Set(sessions.flatMap(s => s.tags || []))].sort();
|
|
281
|
+
const chip = (label,val,on) => `<button data-tag="${val==null?'':esc(val)}" style="border:1px solid ${on?'var(--blue2)':'var(--border)'};background:${on?'var(--blue2)':'var(--bg)'};color:${on?'#fff':'var(--muted)'};border-radius:999px;font-size:0.7rem;padding:0.12rem 0.5rem;cursor:pointer">${esc(label)}</button>`;
|
|
282
|
+
tagbar.innerHTML = chip('全部',null,!_tpTag) + allTags.map(t=>chip(t,t,_tpTag===t)).join('');
|
|
283
|
+
tagbar.style.display = 'flex';
|
|
284
|
+
} else { tagbar.style.display='none'; }
|
|
285
|
+
|
|
286
|
+
const kw = (document.getElementById('tp-search').value||'').trim().toLowerCase();
|
|
287
|
+
let items = sessions.filter(s => !kw || sName(s).toLowerCase().includes(kw) || (s.cwd||'').toLowerCase().includes(kw) || (s.tags||[]).join(' ').toLowerCase().includes(kw));
|
|
288
|
+
items.sort((a,b)=>sessionTime(b)-sessionTime(a));
|
|
289
|
+
const itemHtml = (s) => {
|
|
290
|
+
const cur = _pickTarget && _pickTarget._sid === sId(s);
|
|
291
|
+
const tags = (s.tags||[]).map(t=>`<span style="background:var(--surface2);color:var(--muted);border-radius:4px;font-size:0.62rem;padding:0.02rem 0.3rem">${esc(t)}</span>`).join(' ');
|
|
292
|
+
// 与首页卡片同款信息:时间/在线/控制中 + 首条消息 + 待发草稿 + 存活状态点
|
|
293
|
+
const meta = [fmtRelTime(sessionTime(s)), s.viewers > 0 ? `${s.viewers} 在线` : '', s.controlHolderName ? `${s.controlHolderName} 控制中` : ''].filter(Boolean).join(' · ');
|
|
294
|
+
const gist = s.firstUserText ? `<div style="font-size:0.66rem;color:var(--muted);margin-top:0.12rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">💬 ${esc(s.firstUserText)}</div>` : '';
|
|
295
|
+
const nDraft = _draftCounts[sId(s)] || 0;
|
|
296
|
+
const draft = nDraft ? `<div style="font-size:0.64rem;color:#d29922;margin-top:0.1rem">📥 ${nDraft} 条待发草稿</div>` : '';
|
|
297
|
+
return `<div data-id="${esc(sId(s))}" style="display:flex;align-items:center;gap:0.5rem;padding:0.45rem 0.5rem;border-radius:7px;cursor:pointer;border:1px solid ${cur?'var(--blue2)':'transparent'};background:${cur?'var(--surface2)':'transparent'}">
|
|
298
|
+
<div style="flex:1;min-width:0">
|
|
299
|
+
<div style="font-size:0.82rem;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(sName(s))} ${tags}</div>
|
|
300
|
+
<div style="font-size:0.66rem;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(s.cwd||'(无目录)')}${meta ? ' · ' + esc(meta) : ''}</div>
|
|
301
|
+
${gist}
|
|
302
|
+
${draft}
|
|
303
|
+
</div>
|
|
304
|
+
<div style="flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:0.2rem">
|
|
305
|
+
<span title="${s.alive ? '存活' : '已结束'}" style="width:8px;height:8px;border-radius:50%;background:${s.alive?'var(--green,#2ea043)':'var(--red,#f85149)'};box-shadow:${s.alive?'0 0 5px var(--green,#2ea043)':'none'}"></span>
|
|
306
|
+
${s.viewers > 1 ? `<span style="font-size:0.6rem;color:var(--dim)">👁 ${s.viewers}</span>` : ''}
|
|
307
|
+
</div>
|
|
308
|
+
</div>`;
|
|
309
|
+
};
|
|
310
|
+
const header = (t) => `<div style="color:var(--muted);font-size:0.7rem;font-weight:600;margin:0.6rem 0.2rem 0.25rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t)}</div>`;
|
|
311
|
+
let html = '';
|
|
312
|
+
if (_tpGroup === 'project') {
|
|
313
|
+
const groups = new Map();
|
|
314
|
+
for (const s of items) pushMap(groups, s.cwd || '(无目录)', s);
|
|
315
|
+
const ordered = [...groups.entries()].sort((a,b)=>sessionTime(b[1][0])-sessionTime(a[1][0]));
|
|
316
|
+
for (const [k,arr] of ordered) html += header(k) + arr.map(itemHtml).join('');
|
|
317
|
+
} else if (_tpGroup === 'tag') {
|
|
318
|
+
if (_tpTag) { html += items.filter(s => (s.tags||[]).includes(_tpTag)).map(itemHtml).join(''); }
|
|
319
|
+
else {
|
|
320
|
+
const tagged = new Map(); const untagged = [];
|
|
321
|
+
for (const s of items) { const ts=s.tags||[]; if(!ts.length) untagged.push(s); for(const t of ts) pushMap(tagged,t,s); }
|
|
322
|
+
const ordered=[...tagged.entries()].sort((a,b)=>sessionTime(b[1][0])-sessionTime(a[1][0]));
|
|
323
|
+
for (const [t,arr] of ordered) html += header('# '+t) + arr.map(itemHtml).join('');
|
|
324
|
+
if (untagged.length) html += header('无标签') + untagged.map(itemHtml).join('');
|
|
325
|
+
}
|
|
326
|
+
} else { html = items.map(itemHtml).join(''); }
|
|
327
|
+
document.getElementById('tp-list').innerHTML = html || '<div style="color:var(--muted);text-align:center;padding:1.5rem 0;font-size:0.8rem">没有匹配的任务</div>';
|
|
328
|
+
}
|
|
329
|
+
// 事件委托:选任务 / 选标签 / 切分组 / 搜索
|
|
330
|
+
document.getElementById('tp-list').addEventListener('click', (e) => {
|
|
331
|
+
const el = e.target.closest('[data-id]'); if (!el) return;
|
|
332
|
+
if (_pickTarget) setPaneSession(_pickTarget, el.dataset.id);
|
|
333
|
+
closeTaskPicker();
|
|
334
|
+
});
|
|
335
|
+
document.getElementById('tp-tagbar').addEventListener('click', (e) => {
|
|
336
|
+
const el = e.target.closest('[data-tag]'); if (!el) return;
|
|
337
|
+
_tpTag = el.dataset.tag || null; renderPicker();
|
|
338
|
+
});
|
|
339
|
+
document.querySelectorAll('#tp-group button').forEach(b => b.addEventListener('click', () => {
|
|
340
|
+
_tpGroup = b.dataset.g; _tpTag = null; localStorage.setItem('myhi_grid_group', _tpGroup); renderPicker();
|
|
341
|
+
}));
|
|
342
|
+
document.getElementById('tp-search').addEventListener('input', renderPicker);
|
|
343
|
+
|
|
344
|
+
// ── 新建任务(和首页「新建终端会话」一致:预设 + 名称 + 命令 + 目录 + 最近目录)──
|
|
345
|
+
const socket = io();
|
|
346
|
+
const NT_PRESETS = [
|
|
347
|
+
{ icon:'🤖', label:'默认', title:'claude', type:'agent', cmd:'' },
|
|
348
|
+
{ icon:'🐚', label:'Shell', title:'shell', type:'pty', cmd:'' },
|
|
349
|
+
];
|
|
350
|
+
let _ntPreset = 0;
|
|
351
|
+
const RECENT_DIRS_KEY = 'myhi_recent_dirs'; // 和首页共用
|
|
352
|
+
function ntRecentDirs(){ try { return JSON.parse(localStorage.getItem(RECENT_DIRS_KEY)) || {}; } catch { return {}; } }
|
|
353
|
+
function ntRecordDir(dir){ if(!dir) return; const d=ntRecentDirs(); d[dir]=(d[dir]||0)+1; const s=Object.entries(d).sort((a,b)=>b[1]-a[1]).slice(0,5); localStorage.setItem(RECENT_DIRS_KEY, JSON.stringify(Object.fromEntries(s))); }
|
|
354
|
+
function ntRenderPresets(){
|
|
355
|
+
document.getElementById('nt-presets').innerHTML = NT_PRESETS.map((p,i)=>`<button data-i="${i}" style="flex:1;min-width:5rem;display:flex;align-items:center;justify-content:center;gap:0.3rem;border:1px solid ${i===_ntPreset?'var(--blue2)':'var(--border)'};background:${i===_ntPreset?'var(--blue2)':'var(--bg)'};color:${i===_ntPreset?'#fff':'var(--text2)'};border-radius:8px;padding:0.4rem;font-size:0.8rem;cursor:pointer">${p.icon} ${esc(p.label)}</button>`).join('');
|
|
356
|
+
}
|
|
357
|
+
function ntRenderRecent(){
|
|
358
|
+
const dirs = Object.entries(ntRecentDirs()).sort((a,b)=>b[1]-a[1]);
|
|
359
|
+
document.getElementById('nt-recent').innerHTML = dirs.map(([p])=>`<button data-dir="${esc(p)}" style="background:var(--surface2);border:1px solid var(--border);color:var(--muted);border-radius:6px;font-size:0.68rem;padding:0.12rem 0.4rem;cursor:pointer;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p)}</button>`).join('');
|
|
360
|
+
}
|
|
361
|
+
window.openNewTask = function(){
|
|
362
|
+
_ntPreset = 0;
|
|
363
|
+
document.getElementById('new-task').style.display = '';
|
|
364
|
+
document.getElementById('nt-title').value = '';
|
|
365
|
+
document.getElementById('nt-cmd').value = '';
|
|
366
|
+
document.getElementById('nt-cwd').value = '';
|
|
367
|
+
ntRenderPresets(); ntRenderRecent();
|
|
368
|
+
setTimeout(()=>document.getElementById('nt-title').focus(), 30);
|
|
369
|
+
};
|
|
370
|
+
window.closeNewTask = function(){ document.getElementById('new-task').style.display='none'; };
|
|
371
|
+
window.submitNewTask = function(){
|
|
372
|
+
const title = document.getElementById('nt-title').value.trim();
|
|
373
|
+
if (!title) { document.getElementById('nt-title').focus(); return; }
|
|
374
|
+
const cmd = document.getElementById('nt-cmd').value.trim() || undefined;
|
|
375
|
+
const cwd = document.getElementById('nt-cwd').value.trim() || undefined;
|
|
376
|
+
if (cwd) ntRecordDir(cwd);
|
|
377
|
+
const preset = NT_PRESETS[_ntPreset];
|
|
378
|
+
const done = (res) => {
|
|
379
|
+
if (res && res.ok && res.session) { closeNewTask(); refreshSessions(true).then(()=>addPane(res.session.id)); }
|
|
380
|
+
else alert('创建失败:' + ((res && res.error) || '未知错误'));
|
|
381
|
+
};
|
|
382
|
+
// 工作目录留空时:让服务端按任务名建在「默认目录/任务名」下,避免多任务全堆在同一默认目录
|
|
383
|
+
const nestByTitle = !cwd;
|
|
384
|
+
if (preset.type === 'agent') socket.emit('create-agent', { title, cwd, nestByTitle }, done);
|
|
385
|
+
else socket.emit('create', { title, initCmd: cmd, cwd, nestByTitle }, done);
|
|
386
|
+
};
|
|
387
|
+
document.getElementById('nt-presets').addEventListener('click', (e) => {
|
|
388
|
+
const b = e.target.closest('[data-i]'); if (!b) return;
|
|
389
|
+
_ntPreset = +b.dataset.i; const p = NT_PRESETS[_ntPreset];
|
|
390
|
+
const t = document.getElementById('nt-title');
|
|
391
|
+
if (!t.value.trim() || NT_PRESETS.some(x => x.title === t.value.trim())) t.value = p.title;
|
|
392
|
+
document.getElementById('nt-cmd').value = p.cmd || '';
|
|
393
|
+
ntRenderPresets();
|
|
394
|
+
});
|
|
395
|
+
document.getElementById('nt-recent').addEventListener('click', (e) => {
|
|
396
|
+
const b = e.target.closest('[data-dir]'); if (!b) return;
|
|
397
|
+
document.getElementById('nt-cwd').value = b.dataset.dir;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ── 窗格状态灯 + 完成/待回答高亮 + 通知(内嵌会话页 postMessage 上报)──
|
|
401
|
+
let _notify = localStorage.getItem('myhi_grid_notify') === '1';
|
|
402
|
+
function syncNotifyBtn(){ const b=document.getElementById('notify-btn'); if(b) b.textContent='🔔 '+(_notify?'开':'关'); }
|
|
403
|
+
window.toggleNotify = function(){
|
|
404
|
+
_notify = !_notify;
|
|
405
|
+
if (_notify && 'Notification' in window && Notification.permission==='default') Notification.requestPermission();
|
|
406
|
+
localStorage.setItem('myhi_grid_notify', _notify?'1':'0');
|
|
407
|
+
syncNotifyBtn();
|
|
408
|
+
};
|
|
409
|
+
syncNotifyBtn();
|
|
410
|
+
function paneTitle(pane){ const s = pane._sid ? sessById(pane._sid) : null; return s ? sName(s) : '任务'; }
|
|
411
|
+
function beep(){ try { const c=new (window.AudioContext||window.webkitAudioContext)(); const o=c.createOscillator(), g=c.createGain(); o.connect(g); g.connect(c.destination); o.frequency.value=660; g.gain.value=0.06; o.start(); setTimeout(()=>{o.stop();c.close();},180);} catch {} }
|
|
412
|
+
function fireNotify(pane,label){
|
|
413
|
+
if(!_notify) return;
|
|
414
|
+
try { if('Notification' in window && Notification.permission==='granted') new Notification((localStorage.getItem('myhi_machine_name')||'myhi')+' · '+label,{body:paneTitle(pane)}); } catch {}
|
|
415
|
+
beep();
|
|
416
|
+
}
|
|
417
|
+
function setPaneStatus(pane,state){
|
|
418
|
+
const prev=pane._state; pane._state=state;
|
|
419
|
+
if(pane._dot){
|
|
420
|
+
const c={idle:'var(--green)',working:'var(--blue)',thinking:'var(--blue)',waiting:'#d29922'}[state]||'var(--dim)';
|
|
421
|
+
pane._dot.style.background=c;
|
|
422
|
+
pane._dot.classList.toggle('pulse', state==='working'||state==='thinking');
|
|
423
|
+
pane._dot.title={idle:'空闲/已完成',working:'执行中',thinking:'执行中',waiting:'等你回答'}[state]||'状态';
|
|
424
|
+
}
|
|
425
|
+
const justDone=(prev==='working'||prev==='thinking')&&state==='idle';
|
|
426
|
+
if(justDone||state==='waiting'){
|
|
427
|
+
if(pane._head){ pane._head.classList.remove('flash'); void pane._head.offsetWidth; pane._head.classList.add('flash'); setTimeout(()=>pane._head&&pane._head.classList.remove('flash'),3200); }
|
|
428
|
+
fireNotify(pane, state==='waiting'?'待回答':'已完成');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
window.addEventListener('message',(e)=>{
|
|
432
|
+
const m=e.data;
|
|
433
|
+
if(!m||m.type!=='myhi-status') return;
|
|
434
|
+
const pane=paneEls.find(p=>p._sid&&p._sid===m.sessionId);
|
|
435
|
+
if(pane) setPaneStatus(pane,m.state);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ── 初始化 ──
|
|
439
|
+
(async function init() {
|
|
440
|
+
await refreshSessions(false);
|
|
441
|
+
const initial = (state.panes && state.panes.length) ? state.panes : [];
|
|
442
|
+
// 用稳定标识映射回当前 id;找不到→空格(可下拉重选),不再加载死会话报「未找到」
|
|
443
|
+
for (const key of initial) addPane(resolveKey(key), true);
|
|
444
|
+
applyLayout();
|
|
445
|
+
})();
|
|
446
|
+
</script>
|
|
447
|
+
</body>
|
|
448
|
+
</html>
|