@wendongfly/myhi 1.0.1

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.
@@ -0,0 +1,871 @@
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, maximum-scale=1.0, viewport-fit=cover">
6
+ <meta name="theme-color" content="#0d1117">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
+ <meta name="apple-mobile-web-app-title" content="myhi">
10
+ <link rel="manifest" href="/manifest.json">
11
+ <link rel="apple-touch-icon" href="/icon.png">
12
+ <title>myhi</title>
13
+ <script src="/socket.io/socket.io.js"></script>
14
+ <style>
15
+ :root {
16
+ --bg: #0d1117;
17
+ --surface: #161b22;
18
+ --border: #21262d;
19
+ --accent: #7c3aed;
20
+ --accent2: #9d5cf5;
21
+ --text: #e6edf3;
22
+ --muted: #8b949e;
23
+ --green: #3fb950;
24
+ --red: #f85149;
25
+ --yellow: #d29922;
26
+ }
27
+ * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
28
+
29
+ html, body {
30
+ height: 100%;
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
34
+ overscroll-behavior: none;
35
+ }
36
+
37
+ /* ── Layout ── */
38
+ #app {
39
+ display: flex;
40
+ flex-direction: column;
41
+ height: 100%;
42
+ max-width: 640px;
43
+ margin: 0 auto;
44
+ }
45
+
46
+ /* ── Header ── */
47
+ #header {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ padding: 0 1rem;
52
+ padding-top: max(1rem, env(safe-area-inset-top));
53
+ padding-bottom: 0.75rem;
54
+ background: var(--bg);
55
+ position: sticky;
56
+ top: 0;
57
+ z-index: 10;
58
+ border-bottom: 1px solid var(--border);
59
+ }
60
+ .logo {
61
+ font-size: 1.35rem;
62
+ font-weight: 700;
63
+ letter-spacing: -0.5px;
64
+ }
65
+ .logo span { color: var(--accent); }
66
+ #conn-dot {
67
+ width: 8px; height: 8px;
68
+ border-radius: 50%;
69
+ background: var(--muted);
70
+ transition: background 0.3s;
71
+ flex-shrink: 0;
72
+ }
73
+ #conn-dot.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
74
+ #conn-dot.off { background: var(--red); }
75
+
76
+ /* ── Session list ── */
77
+ #list {
78
+ flex: 1;
79
+ overflow-y: auto;
80
+ -webkit-overflow-scrolling: touch;
81
+ padding: 0.75rem 0.75rem 6rem;
82
+ }
83
+
84
+ .empty-state {
85
+ display: flex;
86
+ flex-direction: column;
87
+ align-items: center;
88
+ justify-content: center;
89
+ gap: 0.75rem;
90
+ height: 60%;
91
+ color: var(--muted);
92
+ }
93
+ .empty-state svg { opacity: 0.3; }
94
+ .empty-state p { font-size: 0.95rem; }
95
+
96
+ /* ── Session card ── */
97
+ .card {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.85rem;
101
+ padding: 0.85rem 0.9rem;
102
+ background: var(--surface);
103
+ border: 1px solid var(--border);
104
+ border-radius: 12px;
105
+ margin-bottom: 0.6rem;
106
+ cursor: pointer;
107
+ transition: border-color 0.15s, background 0.15s;
108
+ user-select: none;
109
+ -webkit-user-select: none;
110
+ }
111
+ .card:active { background: #1c2230; }
112
+
113
+ .avatar {
114
+ width: 44px; height: 44px;
115
+ border-radius: 10px;
116
+ display: flex; align-items: center; justify-content: center;
117
+ font-size: 1.3rem;
118
+ font-weight: 700;
119
+ flex-shrink: 0;
120
+ font-family: 'SF Mono', Consolas, monospace;
121
+ color: #fff;
122
+ }
123
+
124
+ .card-body {
125
+ flex: 1;
126
+ min-width: 0;
127
+ }
128
+ .card-title {
129
+ font-size: 0.95rem;
130
+ font-weight: 600;
131
+ white-space: nowrap;
132
+ overflow: hidden;
133
+ text-overflow: ellipsis;
134
+ }
135
+ .card-sub {
136
+ font-size: 0.75rem;
137
+ color: var(--muted);
138
+ margin-top: 0.2rem;
139
+ white-space: nowrap;
140
+ overflow: hidden;
141
+ text-overflow: ellipsis;
142
+ }
143
+
144
+ .card-right {
145
+ display: flex;
146
+ flex-direction: column;
147
+ align-items: flex-end;
148
+ gap: 0.35rem;
149
+ flex-shrink: 0;
150
+ }
151
+ .status-dot {
152
+ width: 8px; height: 8px;
153
+ border-radius: 50%;
154
+ }
155
+ .status-dot.alive { background: var(--green); box-shadow: 0 0 5px var(--green); }
156
+ .status-dot.dead { background: var(--red); }
157
+ .viewers {
158
+ font-size: 0.7rem;
159
+ color: var(--muted);
160
+ }
161
+
162
+ /* ── FAB ── */
163
+ #fab {
164
+ position: fixed;
165
+ bottom: max(1.5rem, calc(env(safe-area-inset-bottom) + 1rem));
166
+ right: max(1.5rem, calc(env(safe-area-inset-right) + 1rem));
167
+ width: 56px; height: 56px;
168
+ border-radius: 28px;
169
+ background: var(--accent);
170
+ color: #fff;
171
+ border: none;
172
+ font-size: 1.6rem;
173
+ line-height: 1;
174
+ cursor: pointer;
175
+ box-shadow: 0 4px 20px rgba(124,58,237,0.5);
176
+ display: flex; align-items: center; justify-content: center;
177
+ transition: background 0.15s, transform 0.15s;
178
+ z-index: 20;
179
+ }
180
+ #fab:active { background: var(--accent2); transform: scale(0.93); }
181
+
182
+ /* ── Bottom sheet ── */
183
+ #sheet-backdrop {
184
+ display: none;
185
+ position: fixed; inset: 0;
186
+ background: rgba(0,0,0,0.55);
187
+ z-index: 30;
188
+ }
189
+ #sheet-backdrop.open { display: block; }
190
+
191
+ #sheet {
192
+ position: fixed;
193
+ bottom: 0; left: 0; right: 0;
194
+ background: var(--surface);
195
+ border-radius: 20px 20px 0 0;
196
+ padding: 0 1.25rem;
197
+ padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
198
+ z-index: 31;
199
+ transform: translateY(100%);
200
+ transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
201
+ max-width: 640px;
202
+ margin: 0 auto;
203
+ }
204
+ #sheet.open { transform: translateY(0); }
205
+
206
+ .sheet-handle {
207
+ width: 36px; height: 4px;
208
+ background: var(--border);
209
+ border-radius: 2px;
210
+ margin: 0.75rem auto 1.25rem;
211
+ }
212
+ .sheet-title {
213
+ font-size: 1rem;
214
+ font-weight: 600;
215
+ margin-bottom: 1rem;
216
+ }
217
+
218
+ /* ── Preset chips ── */
219
+ .presets {
220
+ display: flex;
221
+ gap: 0.5rem;
222
+ overflow-x: auto;
223
+ padding-bottom: 0.75rem;
224
+ margin-bottom: 0.75rem;
225
+ border-bottom: 1px solid var(--border);
226
+ scrollbar-width: none;
227
+ -webkit-overflow-scrolling: touch;
228
+ }
229
+ .presets::-webkit-scrollbar { display: none; }
230
+ .chip {
231
+ display: flex;
232
+ flex-direction: column;
233
+ align-items: center;
234
+ gap: 0.3rem;
235
+ flex-shrink: 0;
236
+ width: 76px;
237
+ padding: 0.6rem 0.4rem;
238
+ background: var(--bg);
239
+ border: 1.5px solid var(--border);
240
+ border-radius: 12px;
241
+ cursor: pointer;
242
+ transition: border-color 0.15s, background 0.15s;
243
+ user-select: none;
244
+ -webkit-user-select: none;
245
+ }
246
+ .chip:active, .chip.selected {
247
+ border-color: var(--accent);
248
+ background: rgba(124,58,237,0.12);
249
+ }
250
+ .chip-icon { font-size: 1.4rem; line-height: 1; }
251
+ .chip-label {
252
+ font-size: 0.65rem;
253
+ color: var(--muted);
254
+ text-align: center;
255
+ white-space: nowrap;
256
+ overflow: hidden;
257
+ text-overflow: ellipsis;
258
+ width: 100%;
259
+ }
260
+ .chip.selected .chip-label { color: var(--accent2); }
261
+
262
+ .field { margin-bottom: 0.9rem; }
263
+ .field label {
264
+ display: block;
265
+ font-size: 0.75rem;
266
+ color: var(--muted);
267
+ margin-bottom: 0.35rem;
268
+ font-weight: 500;
269
+ text-transform: uppercase;
270
+ letter-spacing: 0.04em;
271
+ }
272
+ .field input {
273
+ width: 100%;
274
+ background: var(--bg);
275
+ border: 1px solid var(--border);
276
+ border-radius: 8px;
277
+ padding: 0.65rem 0.8rem;
278
+ color: var(--text);
279
+ font-size: 0.95rem;
280
+ font-family: inherit;
281
+ outline: none;
282
+ transition: border-color 0.15s;
283
+ }
284
+ .field input:focus { border-color: var(--accent); }
285
+ .field input::placeholder { color: #4a5568; }
286
+
287
+ #btn-create {
288
+ width: 100%;
289
+ background: var(--accent);
290
+ color: #fff;
291
+ border: none;
292
+ border-radius: 10px;
293
+ padding: 0.8rem;
294
+ font-size: 0.95rem;
295
+ font-weight: 600;
296
+ cursor: pointer;
297
+ margin-top: 0.25rem;
298
+ transition: background 0.15s;
299
+ }
300
+ #btn-create:active { background: var(--accent2); }
301
+
302
+ /* ── Dir picker ── */
303
+ #dir-picker {
304
+ display: none; position: fixed; inset: 0; z-index: 50;
305
+ align-items: flex-end; justify-content: center;
306
+ }
307
+ #dir-picker.open { display: flex; }
308
+ #dir-backdrop2 { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
309
+ #dir-box {
310
+ position: relative; z-index: 1;
311
+ background: var(--surface);
312
+ border-radius: 20px 20px 0 0;
313
+ width: 100%; max-width: 640px;
314
+ padding: 0 1.25rem;
315
+ padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
316
+ max-height: 70vh; display: flex; flex-direction: column;
317
+ }
318
+ #dir-current {
319
+ font-size: 0.72rem; color: var(--muted);
320
+ font-family: 'SF Mono', Consolas, monospace;
321
+ padding: 0.9rem 0 0.5rem;
322
+ border-bottom: 1px solid var(--border);
323
+ word-break: break-all;
324
+ }
325
+ #dir-list {
326
+ flex: 1; overflow-y: auto; padding: 0.4rem 0;
327
+ -webkit-overflow-scrolling: touch;
328
+ }
329
+ .dir-item {
330
+ display: flex; align-items: center; gap: 0.6rem;
331
+ padding: 0.65rem 0.2rem;
332
+ border-bottom: 1px solid var(--border);
333
+ cursor: pointer; font-size: 0.9rem;
334
+ }
335
+ .dir-item:active { background: var(--border); border-radius: 6px; }
336
+ .dir-item .di { font-size: 1rem; }
337
+ .dir-item .dn { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
338
+ #dir-actions {
339
+ display: flex; gap: 0.6rem; padding: 0.75rem 0;
340
+ border-top: 1px solid var(--border);
341
+ }
342
+ .dir-btn {
343
+ flex: 1; padding: 0.7rem; border-radius: 10px;
344
+ border: none; font-size: 0.9rem; font-weight: 600; cursor: pointer;
345
+ }
346
+ .dir-btn.primary { background: var(--accent); color: #fff; }
347
+ .dir-btn.cancel { background: var(--bg); color: var(--text); }
348
+
349
+ /* ── Kill confirm ── */
350
+ #kill-menu {
351
+ display: none;
352
+ position: fixed; inset: 0;
353
+ z-index: 40;
354
+ align-items: flex-end;
355
+ justify-content: center;
356
+ }
357
+ #kill-menu.open { display: flex; }
358
+ #kill-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.55); }
359
+ #kill-box {
360
+ position: relative;
361
+ background: var(--surface);
362
+ border-radius: 16px 16px 0 0;
363
+ width: 100%;
364
+ max-width: 640px;
365
+ padding: 1.25rem;
366
+ padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
367
+ z-index: 1;
368
+ }
369
+ #kill-box h3 { font-size: 0.95rem; margin-bottom: 0.3rem; }
370
+ #kill-box p { font-size: 0.8rem; color: var(--muted); margin-bottom: 1rem; }
371
+ .kill-btn {
372
+ display: block; width: 100%;
373
+ padding: 0.75rem; border-radius: 10px;
374
+ border: none; font-size: 0.95rem; font-weight: 600;
375
+ cursor: pointer; margin-bottom: 0.5rem;
376
+ }
377
+ .kill-btn.danger { background: #3d1515; color: var(--red); }
378
+ .kill-btn.cancel { background: var(--bg); color: var(--text); }
379
+
380
+ /* ── 导入会话 ── */
381
+ #import-menu {
382
+ display: none; position: fixed; inset: 0; z-index: 40;
383
+ align-items: flex-end; justify-content: center;
384
+ }
385
+ #import-menu.open { display: flex; }
386
+ .import-item {
387
+ display: flex; align-items: center; gap: 0.6rem;
388
+ padding: 0.7rem 0.2rem; border-bottom: 1px solid var(--border);
389
+ cursor: pointer; font-size: 0.85rem;
390
+ }
391
+ .import-item:active { background: var(--border); border-radius: 6px; }
392
+ .import-item .ii-icon { font-size: 1.1rem; }
393
+ .import-item .ii-body { flex: 1; min-width: 0; }
394
+ .import-item .ii-id { font-size: 0.82rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
395
+ .import-item .ii-info { font-size: 0.72rem; color: var(--muted); }
396
+
397
+ /* ── 盘符选择栏 ── */
398
+ #drive-bar {
399
+ display: flex; gap: 0.35rem; padding: 0.5rem 0; border-bottom: 1px solid var(--border);
400
+ overflow-x: auto; scrollbar-width: none;
401
+ }
402
+ #drive-bar::-webkit-scrollbar { display: none; }
403
+ .drive-btn {
404
+ padding: 0.3rem 0.7rem; border-radius: 6px; font-size: 0.78rem; font-weight: 600;
405
+ background: var(--bg); border: 1px solid var(--border); color: var(--text);
406
+ cursor: pointer; white-space: nowrap; flex-shrink: 0;
407
+ }
408
+ .drive-btn:active, .drive-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(124,58,237,0.12); }
409
+ </style>
410
+ </head>
411
+ <body>
412
+ <div id="app">
413
+ <div id="header">
414
+ <div class="logo">my<span>hi</span></div>
415
+ <div id="conn-dot"></div>
416
+ </div>
417
+
418
+ <div id="list">
419
+ <div class="empty-state">
420
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
421
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
422
+ <path d="M8 9l4 3-4 3M13 15h3"/>
423
+ </svg>
424
+ <p>还没有终端会话</p>
425
+ </div>
426
+ </div>
427
+ </div>
428
+
429
+ <!-- FAB -->
430
+ <button id="fab" onclick="openSheet()">+</button>
431
+
432
+ <!-- Create sheet -->
433
+ <div id="sheet-backdrop" onclick="closeSheet()"></div>
434
+ <div id="sheet">
435
+ <div class="sheet-handle"></div>
436
+ <div class="sheet-title">新建终端会话</div>
437
+
438
+ <!-- Preset chips -->
439
+ <div class="presets" id="presets"></div>
440
+
441
+ <div class="field">
442
+ <label>名称</label>
443
+ <input id="inp-title" type="text" placeholder="自定义名称…" autocomplete="off" spellcheck="false">
444
+ </div>
445
+ <div class="field">
446
+ <label>启动命令 <span style="font-weight:400;text-transform:none">(可选)</span></label>
447
+ <input id="inp-cmd" type="text" placeholder="留空仅打开 shell" autocomplete="off" spellcheck="false" style="font-family:'SF Mono',Consolas,monospace;font-size:0.85rem">
448
+ </div>
449
+ <div class="field">
450
+ <label>工作目录 <span style="font-weight:400;text-transform:none">(可选)</span></label>
451
+ <div style="display:flex;gap:0.5rem">
452
+ <input id="inp-cwd" type="text" placeholder="留空使用默认目录" autocomplete="off" spellcheck="false" style="flex:1">
453
+ <button onclick="openDirPicker()" style="flex-shrink:0;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:0 0.75rem;color:var(--text);font-size:1.1rem;cursor:pointer">📁</button>
454
+ </div>
455
+ <div id="recent-dirs" style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.4rem"></div>
456
+ </div>
457
+ <button id="btn-create" onclick="createSession()">创建</button>
458
+ <button id="btn-import" onclick="openImportSheet()" style="width:100%;background:var(--bg);color:var(--accent);border:1.5px solid var(--accent);border-radius:10px;padding:0.7rem;font-size:0.85rem;font-weight:600;cursor:pointer;margin-top:0.5rem">导入已有 Claude 会话</button>
459
+ </div>
460
+
461
+ <!-- Dir picker -->
462
+ <div id="dir-picker">
463
+ <div id="dir-backdrop2" onclick="closeDirPicker()"></div>
464
+ <div id="dir-box">
465
+ <div class="sheet-handle"></div>
466
+ <div id="drive-bar"></div>
467
+ <div id="dir-current"></div>
468
+ <div id="dir-list"></div>
469
+ <div id="dir-actions">
470
+ <button class="dir-btn cancel" onclick="closeDirPicker()">取消</button>
471
+ <button class="dir-btn primary" onclick="selectDir()">选择此目录</button>
472
+ </div>
473
+ </div>
474
+ </div>
475
+
476
+ <!-- Kill confirm sheet -->
477
+ <div id="kill-menu">
478
+ <div id="kill-backdrop" onclick="closeKill()"></div>
479
+ <div id="kill-box">
480
+ <h3 id="kill-title"></h3>
481
+ <p>终止后会话将无法恢复</p>
482
+ <button class="kill-btn danger" onclick="confirmKill()">终止会话</button>
483
+ <button class="kill-btn cancel" onclick="closeKill()">取消</button>
484
+ </div>
485
+ </div>
486
+
487
+ <!-- 导入已有会话 -->
488
+ <div id="import-menu">
489
+ <div id="import-backdrop" onclick="closeImportSheet()" style="position:absolute;inset:0;background:rgba(0,0,0,0.55)"></div>
490
+ <div id="import-box" style="position:relative;background:var(--surface);border-radius:20px 20px 0 0;width:100%;max-width:640px;padding:0 1.25rem;padding-bottom:max(1.5rem,env(safe-area-inset-bottom));max-height:70vh;display:flex;flex-direction:column;z-index:1">
491
+ <div class="sheet-handle"></div>
492
+ <div class="sheet-title">导入已有 Claude 会话</div>
493
+ <div id="import-list" style="flex:1;overflow-y:auto;padding:0.4rem 0;-webkit-overflow-scrolling:touch"></div>
494
+ <div style="padding:0.75rem 0;border-top:1px solid var(--border)">
495
+ <button class="dir-btn cancel" onclick="closeImportSheet()" style="width:100%">取消</button>
496
+ </div>
497
+ </div>
498
+ </div>
499
+
500
+ <script>
501
+ const socket = io({ transports: ['websocket'] });
502
+
503
+ const connDot = document.getElementById('conn-dot');
504
+ const list = document.getElementById('list');
505
+
506
+ // HTML 转义,防止 XSS
507
+ function esc(s) {
508
+ const d = document.createElement('div');
509
+ d.textContent = s;
510
+ return d.innerHTML;
511
+ }
512
+
513
+ // ── Connection ──────────────────────────────────────────────
514
+ socket.on('connect', () => {
515
+ connDot.className = 'on';
516
+ socket.emit('list');
517
+ });
518
+ socket.on('disconnect', () => { connDot.className = 'off'; });
519
+ socket.on('sessions', renderSessions);
520
+
521
+ // Refresh periodically
522
+ setInterval(() => { if (socket.connected) socket.emit('list'); }, 5000);
523
+
524
+ // ── Render ──────────────────────────────────────────────────
525
+ const PALETTE = ['#7c3aed','#2563eb','#059669','#b45309','#db2777','#0891b2'];
526
+
527
+ function avatarColor(title) {
528
+ let h = 0;
529
+ for (const c of (title || '?')) h = (h * 31 + c.charCodeAt(0)) & 0xff;
530
+ return PALETTE[h % PALETTE.length];
531
+ }
532
+
533
+ function fmtCwd(cwd) {
534
+ if (!cwd) return '';
535
+ // Show last 2 path segments
536
+ const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean);
537
+ return parts.slice(-2).join('/');
538
+ }
539
+
540
+ function fmtTime(iso) {
541
+ const d = new Date(iso);
542
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
543
+ }
544
+
545
+ function renderSessions(sessions) {
546
+ if (!sessions.length) {
547
+ list.innerHTML = `<div class="empty-state">
548
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
549
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
550
+ <path d="M8 9l4 3-4 3M13 15h3"/>
551
+ </svg>
552
+ <p>还没有终端会话</p>
553
+ </div>`;
554
+ return;
555
+ }
556
+
557
+ list.innerHTML = sessions.map(s => {
558
+ const safeTitle = esc(s.title || '');
559
+ const safeId = esc(s.id);
560
+ const color = avatarColor(s.title);
561
+ const letter = esc((s.title || '?')[0].toUpperCase());
562
+ const sub = esc([fmtCwd(s.cwd), fmtTime(s.createdAt), s.viewers > 0 ? `${s.viewers} 在线` : '', s.controlHolderName ? `${s.controlHolderName} 控制中` : ''].filter(Boolean).join(' · '));
563
+ return `
564
+ <div class="card" data-id="${safeId}" data-title="${safeTitle}">
565
+ <div class="avatar" style="background:${color}">${letter}</div>
566
+ <div class="card-body">
567
+ <div class="card-title">${safeTitle}</div>
568
+ <div class="card-sub">${sub}</div>
569
+ </div>
570
+ <div class="card-right">
571
+ <div class="status-dot ${s.alive ? 'alive' : 'dead'}"></div>
572
+ ${s.viewers > 1 ? `<div class="viewers">👁 ${s.viewers}</div>` : ''}
573
+ </div>
574
+ </div>`;
575
+ }).join('');
576
+
577
+ // 用事件委托替代内联 onclick/oncontextmenu
578
+ list.querySelectorAll('.card').forEach(card => {
579
+ const id = card.dataset.id;
580
+ const title = card.dataset.title;
581
+ card.addEventListener('click', () => openSession(id));
582
+ card.addEventListener('contextmenu', (e) => showKill(e, id, title));
583
+ });
584
+ }
585
+
586
+ // ── Navigation ──────────────────────────────────────────────
587
+ function openSession(id) {
588
+ window.location.href = `/terminal/${id}`;
589
+ }
590
+
591
+ // ── Presets ───────────────────────────────────────────────────
592
+ const PRESETS = [
593
+ // Agent 模式(使用 Claude SDK)
594
+ { icon: '🤖', label: 'Claude', title: 'claude', type: 'agent', cmd: 'claude', permissionMode: 'default' },
595
+ { icon: '✏️', label: '自动编辑', title: 'claude', type: 'agent', cmd: 'claude --permission-mode acceptEdits', permissionMode: 'acceptEdits' },
596
+ { icon: '📋', label: '计划模式', title: 'claude', type: 'agent', cmd: 'claude --permission-mode plan', permissionMode: 'plan' },
597
+ { icon: '⚡', label: '跳过权限', title: 'claude', type: 'agent', cmd: 'claude --dangerously-skip-permissions', permissionMode: 'bypassPermissions' },
598
+ // PTY 模式(终端)
599
+ { icon: '✨', label: 'Gemini', title: 'gemini', type: 'pty', cmd: 'gemini' },
600
+ { icon: '🐚', label: 'Shell', title: 'shell', type: 'pty', cmd: '' },
601
+ ];
602
+
603
+ let selectedPreset = null;
604
+
605
+ function renderPresets() {
606
+ const el = document.getElementById('presets');
607
+ el.innerHTML = PRESETS.map((p, i) => `
608
+ <div class="chip" id="chip-${i}" onclick="selectPreset(${i})">
609
+ <span class="chip-icon">${p.icon}</span>
610
+ <span class="chip-label">${p.label}</span>
611
+ </div>`).join('');
612
+ }
613
+ renderPresets();
614
+
615
+ function selectPreset(i) {
616
+ selectedPreset = i;
617
+ document.querySelectorAll('.chip').forEach((c, j) =>
618
+ c.classList.toggle('selected', j === i));
619
+ const p = PRESETS[i];
620
+ document.getElementById('inp-title').value = p.title;
621
+ document.getElementById('inp-cmd').value = p.cmd || '';
622
+ }
623
+
624
+ // ── Create sheet ─────────────────────────────────────────────
625
+ function openSheet() {
626
+ document.getElementById('sheet-backdrop').classList.add('open');
627
+ document.getElementById('sheet').classList.add('open');
628
+ // Default to first preset
629
+ selectPreset(0);
630
+ setTimeout(() => document.getElementById('inp-title').focus(), 300);
631
+ }
632
+ function closeSheet() {
633
+ document.getElementById('sheet-backdrop').classList.remove('open');
634
+ document.getElementById('sheet').classList.remove('open');
635
+ selectedPreset = null;
636
+ document.querySelectorAll('.chip').forEach(c => c.classList.remove('selected'));
637
+ }
638
+
639
+ document.getElementById('inp-title').addEventListener('keydown', e => {
640
+ if (e.key === 'Enter') createSession();
641
+ });
642
+
643
+ function createSession() {
644
+ const title = document.getElementById('inp-title').value.trim() || 'shell';
645
+ const initCmd = document.getElementById('inp-cmd').value.trim() || undefined;
646
+ const cwd = document.getElementById('inp-cwd').value.trim() || undefined;
647
+ if (cwd) recordDir(cwd);
648
+ const preset = selectedPreset != null ? PRESETS[selectedPreset] : null;
649
+ closeSheet();
650
+ document.getElementById('inp-title').value = '';
651
+ document.getElementById('inp-cmd').value = '';
652
+ document.getElementById('inp-cwd').value = '';
653
+
654
+ if (preset?.type === 'agent') {
655
+ // Agent 模式:使用 Claude SDK
656
+ socket.emit('create-agent', {
657
+ title,
658
+ cwd,
659
+ permissionMode: preset.permissionMode,
660
+ }, (res) => {
661
+ if (res?.ok) openSession(res.session.id);
662
+ else alert('创建失败: ' + (res?.error || '未知错误'));
663
+ });
664
+ } else {
665
+ // PTY 模式:创建终端会话
666
+ socket.emit('create', { title, initCmd, cwd }, (res) => {
667
+ if (res?.ok) openSession(res.session.id);
668
+ else alert('创建失败: ' + (res?.error || '未知错误'));
669
+ });
670
+ }
671
+ }
672
+
673
+ // ── Dir picker ───────────────────────────────────────────────
674
+ let _dirCurrent = '';
675
+ const RECENT_DIRS_KEY = 'myhi_recent_dirs'; // { path: count } 频次记录
676
+ const RECENT_DIRS_MAX = 5;
677
+
678
+ function getRecentDirs() {
679
+ try { return JSON.parse(localStorage.getItem(RECENT_DIRS_KEY)) || {}; } catch { return {}; }
680
+ }
681
+ function recordDir(dir) {
682
+ if (!dir) return;
683
+ const dirs = getRecentDirs();
684
+ dirs[dir] = (dirs[dir] || 0) + 1;
685
+ // 只保留使用最多的若干条
686
+ const sorted = Object.entries(dirs).sort((a, b) => b[1] - a[1]);
687
+ const trimmed = Object.fromEntries(sorted.slice(0, RECENT_DIRS_MAX));
688
+ localStorage.setItem(RECENT_DIRS_KEY, JSON.stringify(trimmed));
689
+ renderRecentDirs();
690
+ }
691
+ function renderRecentDirs() {
692
+ const container = document.getElementById('recent-dirs');
693
+ container.innerHTML = '';
694
+ const dirs = getRecentDirs();
695
+ const sorted = Object.entries(dirs).sort((a, b) => b[1] - a[1]);
696
+ if (!sorted.length) return;
697
+ sorted.forEach(([path]) => {
698
+ const btn = document.createElement('button');
699
+ // 只显示最后一级目录名
700
+ const short = path.replace(/[\\/]$/, '').split(/[\\/]/).pop() || path;
701
+ btn.textContent = short;
702
+ btn.title = path;
703
+ btn.style.cssText = 'background:var(--bg);color:var(--accent);border:1px solid var(--border);border-radius:6px;padding:0.2rem 0.5rem;font-size:0.72rem;cursor:pointer;font-family:"SF Mono",Consolas,monospace;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
704
+ btn.onclick = () => { document.getElementById('inp-cwd').value = path; };
705
+ container.appendChild(btn);
706
+ });
707
+ }
708
+ renderRecentDirs();
709
+
710
+ function openDirPicker() {
711
+ document.getElementById('dir-picker').classList.add('open');
712
+ const dirs = getRecentDirs();
713
+ const topDir = Object.entries(dirs).sort((a, b) => b[1] - a[1])[0]?.[0] || '';
714
+ const initial = document.getElementById('inp-cwd').value.trim() || topDir || '';
715
+ loadDirs(initial);
716
+ loadDrives();
717
+ }
718
+ function closeDirPicker() {
719
+ document.getElementById('dir-picker').classList.remove('open');
720
+ }
721
+ function selectDir() {
722
+ document.getElementById('inp-cwd').value = _dirCurrent;
723
+ recordDir(_dirCurrent);
724
+ closeDirPicker();
725
+ }
726
+
727
+ // Windows 盘符检测和切换
728
+ function loadDrives() {
729
+ const bar = document.getElementById('drive-bar');
730
+ if (!bar) return;
731
+ // 通过尝试访问常见盘符来检测
732
+ const letters = ['C', 'D', 'E', 'F', 'G', 'H'];
733
+ bar.innerHTML = '';
734
+ let found = 0;
735
+ for (const letter of letters) {
736
+ const path = letter + ':\\';
737
+ socket.emit('dirs', path, (data) => {
738
+ if (data.ok) {
739
+ found++;
740
+ const btn = document.createElement('button');
741
+ btn.className = 'drive-btn';
742
+ btn.textContent = letter + ':';
743
+ btn.onclick = () => {
744
+ loadDirs(path);
745
+ bar.querySelectorAll('.drive-btn').forEach(b => b.classList.remove('active'));
746
+ btn.classList.add('active');
747
+ };
748
+ // 高亮当前盘符
749
+ if (_dirCurrent && _dirCurrent.toUpperCase().startsWith(letter + ':')) {
750
+ btn.classList.add('active');
751
+ }
752
+ bar.appendChild(btn);
753
+ }
754
+ });
755
+ }
756
+ }
757
+
758
+ function loadDirs(path) {
759
+ document.getElementById('dir-list').innerHTML = '<div style="padding:1rem;color:var(--muted);font-size:0.85rem">加载中...</div>';
760
+ socket.emit('dirs', path || '', (data) => {
761
+ if (!data.ok) {
762
+ document.getElementById('dir-list').innerHTML = `<div style="padding:1rem;color:var(--red);font-size:0.85rem">${escH(data.error)}</div>`;
763
+ return;
764
+ }
765
+ _dirCurrent = data.current;
766
+ document.getElementById('dir-current').textContent = data.current;
767
+ // 更新盘符高亮
768
+ document.querySelectorAll('.drive-btn').forEach(btn => {
769
+ btn.classList.toggle('active', _dirCurrent.toUpperCase().startsWith(btn.textContent.toUpperCase()));
770
+ });
771
+ const dirList = document.getElementById('dir-list');
772
+ dirList.innerHTML = '';
773
+ if (data.parent) {
774
+ const el = document.createElement('div');
775
+ el.className = 'dir-item';
776
+ el.dataset.path = data.parent;
777
+ el.innerHTML = '<span class="di">↩</span><span class="dn">..</span>';
778
+ dirList.appendChild(el);
779
+ }
780
+ for (const d of data.dirs) {
781
+ const el = document.createElement('div');
782
+ el.className = 'dir-item';
783
+ el.dataset.path = d.path;
784
+ el.innerHTML = `<span class="di">📁</span><span class="dn">${escH(d.name)}</span>`;
785
+ dirList.appendChild(el);
786
+ }
787
+ if (!dirList.children.length) {
788
+ dirList.innerHTML = '<div style="padding:1rem;color:var(--muted);font-size:0.85rem">无子目录</div>';
789
+ }
790
+ });
791
+ }
792
+ document.getElementById('dir-list').addEventListener('click', e => {
793
+ const item = e.target.closest('.dir-item');
794
+ if (item?.dataset.path) loadDirs(item.dataset.path);
795
+ });
796
+ function escH(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
797
+
798
+ // ── 导入已有 Claude 会话 ───────────────────────────────────
799
+ function openImportSheet() {
800
+ closeSheet(); // 关闭创建面板
801
+ document.getElementById('import-menu').classList.add('open');
802
+ document.getElementById('import-list').innerHTML = '<div style="padding:1rem;color:var(--muted);font-size:0.85rem">加载中...</div>';
803
+ // 通过 REST API 获取本地 Claude 会话列表
804
+ fetch('/api/claude-sessions')
805
+ .then(r => r.json())
806
+ .then(sessions => {
807
+ const el = document.getElementById('import-list');
808
+ if (!sessions.length) {
809
+ el.innerHTML = '<div style="padding:1rem;color:var(--muted);font-size:0.85rem">未找到本地 Claude 会话<br><span style="font-size:0.72rem">会话数据存储在 ~/.claude/projects/ 目录</span></div>';
810
+ return;
811
+ }
812
+ el.innerHTML = '';
813
+ for (const s of sessions) {
814
+ const item = document.createElement('div');
815
+ item.className = 'import-item';
816
+ item.onclick = () => importSession(s);
817
+ const time = s.updatedAt ? new Date(s.updatedAt).toLocaleString('zh-CN', { month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit' }) : '';
818
+ const proj = s.projectDir ? decodeURIComponent(s.projectDir).split(/[\\/]/).pop() : '';
819
+ const name = s.summary || s.sessionId.slice(0, 12);
820
+ item.innerHTML = `
821
+ <span class="ii-icon">🤖</span>
822
+ <div class="ii-body">
823
+ <div class="ii-id">${escH(name)}</div>
824
+ <div class="ii-info">${time ? time + ' · ' : ''}${s.messageCount} 条消息${proj ? ' · ' + escH(proj) : ''}</div>
825
+ </div>`;
826
+ el.appendChild(item);
827
+ }
828
+ })
829
+ .catch(err => {
830
+ document.getElementById('import-list').innerHTML = `<div style="padding:1rem;color:var(--red);font-size:0.85rem">加载失败: ${escH(err.message)}</div>`;
831
+ });
832
+ }
833
+
834
+ function closeImportSheet() {
835
+ document.getElementById('import-menu').classList.remove('open');
836
+ }
837
+
838
+ function importSession(session) {
839
+ closeImportSheet();
840
+ const cwd = session.projectPath || document.getElementById('inp-cwd').value.trim() || undefined;
841
+ const title = session.summary ? session.summary.slice(0, 30) : 'claude (导入)';
842
+ socket.emit('create-agent', {
843
+ title,
844
+ cwd,
845
+ resumeSessionId: session.sessionId,
846
+ permissionMode: 'default',
847
+ }, (res) => {
848
+ if (res?.ok) openSession(res.session.id);
849
+ else alert('导入失败: ' + (res?.error || '未知错误'));
850
+ });
851
+ }
852
+
853
+ // ── Kill ─────────────────────────────────────────────────────
854
+ let _killId = null;
855
+ function showKill(e, id, title) {
856
+ e.preventDefault();
857
+ _killId = id;
858
+ document.getElementById('kill-title').textContent = title;
859
+ document.getElementById('kill-menu').classList.add('open');
860
+ }
861
+ function closeKill() {
862
+ document.getElementById('kill-menu').classList.remove('open');
863
+ _killId = null;
864
+ }
865
+ function confirmKill() {
866
+ if (_killId) socket.emit('kill', _killId);
867
+ closeKill();
868
+ }
869
+ </script>
870
+ </body>
871
+ </html>