@wendongfly/zihi 1.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.
Files changed (145) hide show
  1. package/bin/daemon.js +23 -0
  2. package/bin/zihi.js +603 -0
  3. package/dist/admin.html +297 -0
  4. package/dist/attach.js +2 -0
  5. package/dist/chat.html +2254 -0
  6. package/dist/client-dist/socket.io.esm.min.js +7 -0
  7. package/dist/client-dist/socket.io.js +4955 -0
  8. package/dist/client-dist/socket.io.min.js +7 -0
  9. package/dist/client-dist/socket.io.msgpack.min.js +7 -0
  10. package/dist/files.html +722 -0
  11. package/dist/icon.png +0 -0
  12. package/dist/icon.svg +4 -0
  13. package/dist/index.html +976 -0
  14. package/dist/index.js +485 -0
  15. package/dist/lib/ansi_up.js +431 -0
  16. package/dist/lib/xterm/LICENSE +21 -0
  17. package/dist/lib/xterm/README.md +230 -0
  18. package/dist/lib/xterm/css/xterm.css +209 -0
  19. package/dist/lib/xterm/lib/xterm.js +2 -0
  20. package/dist/lib/xterm/lib/xterm.js.map +1 -0
  21. package/dist/lib/xterm/package.json +100 -0
  22. package/dist/lib/xterm/src/browser/AccessibilityManager.ts +300 -0
  23. package/dist/lib/xterm/src/browser/Clipboard.ts +93 -0
  24. package/dist/lib/xterm/src/browser/ColorContrastCache.ts +34 -0
  25. package/dist/lib/xterm/src/browser/Lifecycle.ts +33 -0
  26. package/dist/lib/xterm/src/browser/Linkifier2.ts +416 -0
  27. package/dist/lib/xterm/src/browser/LocalizableStrings.ts +12 -0
  28. package/dist/lib/xterm/src/browser/OscLinkProvider.ts +128 -0
  29. package/dist/lib/xterm/src/browser/RenderDebouncer.ts +83 -0
  30. package/dist/lib/xterm/src/browser/ScreenDprMonitor.ts +72 -0
  31. package/dist/lib/xterm/src/browser/Terminal.ts +1305 -0
  32. package/dist/lib/xterm/src/browser/TimeBasedDebouncer.ts +86 -0
  33. package/dist/lib/xterm/src/browser/Types.d.ts +181 -0
  34. package/dist/lib/xterm/src/browser/Viewport.ts +401 -0
  35. package/dist/lib/xterm/src/browser/decorations/BufferDecorationRenderer.ts +134 -0
  36. package/dist/lib/xterm/src/browser/decorations/ColorZoneStore.ts +117 -0
  37. package/dist/lib/xterm/src/browser/decorations/OverviewRulerRenderer.ts +219 -0
  38. package/dist/lib/xterm/src/browser/input/CompositionHelper.ts +246 -0
  39. package/dist/lib/xterm/src/browser/input/Mouse.ts +54 -0
  40. package/dist/lib/xterm/src/browser/input/MoveToCell.ts +249 -0
  41. package/dist/lib/xterm/src/browser/public/Terminal.ts +260 -0
  42. package/dist/lib/xterm/src/browser/renderer/dom/DomRenderer.ts +506 -0
  43. package/dist/lib/xterm/src/browser/renderer/dom/DomRendererRowFactory.ts +522 -0
  44. package/dist/lib/xterm/src/browser/renderer/dom/WidthCache.ts +157 -0
  45. package/dist/lib/xterm/src/browser/renderer/shared/CellColorResolver.ts +137 -0
  46. package/dist/lib/xterm/src/browser/renderer/shared/CharAtlasCache.ts +96 -0
  47. package/dist/lib/xterm/src/browser/renderer/shared/CharAtlasUtils.ts +75 -0
  48. package/dist/lib/xterm/src/browser/renderer/shared/Constants.ts +14 -0
  49. package/dist/lib/xterm/src/browser/renderer/shared/CursorBlinkStateManager.ts +146 -0
  50. package/dist/lib/xterm/src/browser/renderer/shared/CustomGlyphs.ts +687 -0
  51. package/dist/lib/xterm/src/browser/renderer/shared/DevicePixelObserver.ts +41 -0
  52. package/dist/lib/xterm/src/browser/renderer/shared/README.md +1 -0
  53. package/dist/lib/xterm/src/browser/renderer/shared/RendererUtils.ts +58 -0
  54. package/dist/lib/xterm/src/browser/renderer/shared/SelectionRenderModel.ts +91 -0
  55. package/dist/lib/xterm/src/browser/renderer/shared/TextureAtlas.ts +1082 -0
  56. package/dist/lib/xterm/src/browser/renderer/shared/Types.d.ts +173 -0
  57. package/dist/lib/xterm/src/browser/selection/SelectionModel.ts +144 -0
  58. package/dist/lib/xterm/src/browser/selection/Types.d.ts +15 -0
  59. package/dist/lib/xterm/src/browser/services/CharSizeService.ts +102 -0
  60. package/dist/lib/xterm/src/browser/services/CharacterJoinerService.ts +339 -0
  61. package/dist/lib/xterm/src/browser/services/CoreBrowserService.ts +33 -0
  62. package/dist/lib/xterm/src/browser/services/MouseService.ts +46 -0
  63. package/dist/lib/xterm/src/browser/services/RenderService.ts +284 -0
  64. package/dist/lib/xterm/src/browser/services/SelectionService.ts +1029 -0
  65. package/dist/lib/xterm/src/browser/services/Services.ts +138 -0
  66. package/dist/lib/xterm/src/browser/services/ThemeService.ts +237 -0
  67. package/dist/lib/xterm/src/common/CircularList.ts +241 -0
  68. package/dist/lib/xterm/src/common/Clone.ts +23 -0
  69. package/dist/lib/xterm/src/common/Color.ts +356 -0
  70. package/dist/lib/xterm/src/common/CoreTerminal.ts +284 -0
  71. package/dist/lib/xterm/src/common/EventEmitter.ts +73 -0
  72. package/dist/lib/xterm/src/common/InputHandler.ts +3443 -0
  73. package/dist/lib/xterm/src/common/Lifecycle.ts +108 -0
  74. package/dist/lib/xterm/src/common/MultiKeyMap.ts +42 -0
  75. package/dist/lib/xterm/src/common/Platform.ts +43 -0
  76. package/dist/lib/xterm/src/common/SortedList.ts +118 -0
  77. package/dist/lib/xterm/src/common/TaskQueue.ts +166 -0
  78. package/dist/lib/xterm/src/common/TypedArrayUtils.ts +17 -0
  79. package/dist/lib/xterm/src/common/Types.d.ts +553 -0
  80. package/dist/lib/xterm/src/common/WindowsMode.ts +27 -0
  81. package/dist/lib/xterm/src/common/buffer/AttributeData.ts +196 -0
  82. package/dist/lib/xterm/src/common/buffer/Buffer.ts +654 -0
  83. package/dist/lib/xterm/src/common/buffer/BufferLine.ts +520 -0
  84. package/dist/lib/xterm/src/common/buffer/BufferRange.ts +13 -0
  85. package/dist/lib/xterm/src/common/buffer/BufferReflow.ts +223 -0
  86. package/dist/lib/xterm/src/common/buffer/BufferSet.ts +134 -0
  87. package/dist/lib/xterm/src/common/buffer/CellData.ts +94 -0
  88. package/dist/lib/xterm/src/common/buffer/Constants.ts +149 -0
  89. package/dist/lib/xterm/src/common/buffer/Marker.ts +43 -0
  90. package/dist/lib/xterm/src/common/buffer/Types.d.ts +52 -0
  91. package/dist/lib/xterm/src/common/data/Charsets.ts +256 -0
  92. package/dist/lib/xterm/src/common/data/EscapeSequences.ts +153 -0
  93. package/dist/lib/xterm/src/common/input/Keyboard.ts +398 -0
  94. package/dist/lib/xterm/src/common/input/TextDecoder.ts +346 -0
  95. package/dist/lib/xterm/src/common/input/UnicodeV6.ts +132 -0
  96. package/dist/lib/xterm/src/common/input/WriteBuffer.ts +246 -0
  97. package/dist/lib/xterm/src/common/input/XParseColor.ts +80 -0
  98. package/dist/lib/xterm/src/common/parser/Constants.ts +58 -0
  99. package/dist/lib/xterm/src/common/parser/DcsParser.ts +192 -0
  100. package/dist/lib/xterm/src/common/parser/EscapeSequenceParser.ts +792 -0
  101. package/dist/lib/xterm/src/common/parser/OscParser.ts +238 -0
  102. package/dist/lib/xterm/src/common/parser/Params.ts +229 -0
  103. package/dist/lib/xterm/src/common/parser/Types.d.ts +274 -0
  104. package/dist/lib/xterm/src/common/public/AddonManager.ts +53 -0
  105. package/dist/lib/xterm/src/common/public/BufferApiView.ts +35 -0
  106. package/dist/lib/xterm/src/common/public/BufferLineApiView.ts +29 -0
  107. package/dist/lib/xterm/src/common/public/BufferNamespaceApi.ts +36 -0
  108. package/dist/lib/xterm/src/common/public/ParserApi.ts +37 -0
  109. package/dist/lib/xterm/src/common/public/UnicodeApi.ts +27 -0
  110. package/dist/lib/xterm/src/common/services/BufferService.ts +151 -0
  111. package/dist/lib/xterm/src/common/services/CharsetService.ts +34 -0
  112. package/dist/lib/xterm/src/common/services/CoreMouseService.ts +318 -0
  113. package/dist/lib/xterm/src/common/services/CoreService.ts +87 -0
  114. package/dist/lib/xterm/src/common/services/DecorationService.ts +140 -0
  115. package/dist/lib/xterm/src/common/services/InstantiationService.ts +85 -0
  116. package/dist/lib/xterm/src/common/services/LogService.ts +124 -0
  117. package/dist/lib/xterm/src/common/services/OptionsService.ts +201 -0
  118. package/dist/lib/xterm/src/common/services/OscLinkService.ts +115 -0
  119. package/dist/lib/xterm/src/common/services/ServiceRegistry.ts +49 -0
  120. package/dist/lib/xterm/src/common/services/Services.ts +342 -0
  121. package/dist/lib/xterm/src/common/services/UnicodeService.ts +86 -0
  122. package/dist/lib/xterm/src/headless/Terminal.ts +136 -0
  123. package/dist/lib/xterm/src/headless/public/Terminal.ts +195 -0
  124. package/dist/lib/xterm/typings/xterm.d.ts +1844 -0
  125. package/dist/lib/xterm-fit/LICENSE +19 -0
  126. package/dist/lib/xterm-fit/README.md +24 -0
  127. package/dist/lib/xterm-fit/lib/xterm-addon-fit.js +2 -0
  128. package/dist/lib/xterm-fit/lib/xterm-addon-fit.js.map +1 -0
  129. package/dist/lib/xterm-fit/package.json +26 -0
  130. package/dist/lib/xterm-fit/src/FitAddon.ts +89 -0
  131. package/dist/lib/xterm-fit/typings/xterm-addon-fit.d.ts +55 -0
  132. package/dist/lib/xterm-links/LICENSE +19 -0
  133. package/dist/lib/xterm-links/README.md +21 -0
  134. package/dist/lib/xterm-links/lib/xterm-addon-web-links.js +2 -0
  135. package/dist/lib/xterm-links/lib/xterm-addon-web-links.js.map +1 -0
  136. package/dist/lib/xterm-links/package.json +26 -0
  137. package/dist/lib/xterm-links/src/WebLinkProvider.ts +198 -0
  138. package/dist/lib/xterm-links/src/WebLinksAddon.ts +57 -0
  139. package/dist/lib/xterm-links/typings/xterm-addon-web-links.d.ts +53 -0
  140. package/dist/login.html +163 -0
  141. package/dist/manifest.json +12 -0
  142. package/dist/package.json +1 -0
  143. package/dist/sw.js +127 -0
  144. package/dist/sync.html +816 -0
  145. package/package.json +47 -0
@@ -0,0 +1,976 @@
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="weHi">
10
+ <link rel="manifest" href="/manifest.json">
11
+ <link rel="apple-touch-icon" href="/icon.png">
12
+ <title>weHi</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: #f97316;
20
+ --accent2: #fb923c;
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(249,115,22,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.5rem; padding: 0.6rem 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.5rem 1rem; border-radius: 8px; font-size: 1rem; font-weight: 600;
405
+ background: var(--bg); border: 1.5px solid var(--border); color: var(--text);
406
+ cursor: pointer; white-space: nowrap; flex-shrink: 0;
407
+ min-width: 3rem; text-align: center;
408
+ }
409
+ .drive-btn:active, .drive-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(249,115,22,0.12); }
410
+ #update-btn { background: var(--yellow); color: #000; border: none; border-radius: 6px; padding: 0.35rem 0.75rem; font-size: 0.75rem; font-weight: 600; cursor: pointer; white-space: nowrap; }
411
+ #update-btn:active { opacity: 0.7; }
412
+ #update-btn:disabled { opacity: 0.5; cursor: not-allowed; }
413
+ </style>
414
+ </head>
415
+ <body>
416
+ <div id="app">
417
+ <div id="header">
418
+ <div class="logo">we<span>Hi</span></div>
419
+ <div style="display:flex;align-items:center;gap:0.6rem">
420
+ <span id="user-name" style="font-size:0.8rem;color:var(--muted)"></span>
421
+ <button id="usage-btn" onclick="showUsage()" style="background:none;border:1px solid var(--border);color:var(--muted);font-size:0.75rem;padding:0.3rem 0.6rem;border-radius:6px;cursor:pointer;width:auto">用量</button>
422
+ <button id="logout-btn" onclick="doLogout()" style="display:none;background:none;border:1px solid var(--border);color:var(--muted);font-size:0.75rem;padding:0.3rem 0.6rem;border-radius:6px;cursor:pointer;width:auto">退出</button>
423
+ <div id="conn-dot"></div>
424
+ </div>
425
+ </div>
426
+
427
+ <!-- 升级管理已移至 /admin -->
428
+
429
+ <div id="list">
430
+ <div class="empty-state">
431
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
432
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
433
+ <path d="M8 9l4 3-4 3M13 15h3"/>
434
+ </svg>
435
+ <p>还没有终端会话</p>
436
+ </div>
437
+ </div>
438
+ </div>
439
+
440
+ <!-- FAB -->
441
+ <button id="fab" onclick="openSheet()">+</button>
442
+
443
+ <!-- Create sheet -->
444
+ <div id="sheet-backdrop" onclick="closeSheet()"></div>
445
+ <div id="sheet">
446
+ <div class="sheet-handle"></div>
447
+ <div class="sheet-title">新建终端会话</div>
448
+
449
+ <!-- Preset chips -->
450
+ <div class="presets" id="presets"></div>
451
+
452
+ <div class="field">
453
+ <label>名称</label>
454
+ <input id="inp-title" type="text" placeholder="自定义名称…" autocomplete="off" spellcheck="false">
455
+ </div>
456
+ <div class="field">
457
+ <label>启动命令 <span style="font-weight:400;text-transform:none">(可选)</span></label>
458
+ <input id="inp-cmd" type="text" placeholder="留空仅打开 shell" autocomplete="off" spellcheck="false" style="font-family:'SF Mono',Consolas,monospace;font-size:0.85rem">
459
+ </div>
460
+ <div class="field">
461
+ <label>工作目录 <span style="font-weight:400;text-transform:none">(可选)</span></label>
462
+ <div style="display:flex;gap:0.5rem">
463
+ <input id="inp-cwd" type="text" placeholder="留空使用默认目录" autocomplete="off" spellcheck="false" style="flex:1">
464
+ <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>
465
+ </div>
466
+ <div id="recent-dirs" style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.4rem"></div>
467
+ </div>
468
+ <button id="btn-create" onclick="createSession()">创建</button>
469
+ <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>
470
+ </div>
471
+
472
+ <!-- Dir picker -->
473
+ <div id="dir-picker">
474
+ <div id="dir-backdrop2" onclick="closeDirPicker()"></div>
475
+ <div id="dir-box">
476
+ <div class="sheet-handle"></div>
477
+ <div id="drive-bar"></div>
478
+ <div id="dir-current"></div>
479
+ <div id="dir-list"></div>
480
+ <div id="dir-actions">
481
+ <button class="dir-btn cancel" onclick="closeDirPicker()">取消</button>
482
+ <button class="dir-btn primary" onclick="selectDir()">选择此目录</button>
483
+ </div>
484
+ </div>
485
+ </div>
486
+
487
+ <!-- Kill confirm sheet -->
488
+ <div id="kill-menu">
489
+ <div id="kill-backdrop" onclick="closeKill()"></div>
490
+ <div id="kill-box">
491
+ <h3 id="kill-title"></h3>
492
+ <p>终止后会话将无法恢复</p>
493
+ <button class="kill-btn danger" onclick="confirmKill()">终止会话</button>
494
+ <button class="kill-btn cancel" onclick="closeKill()">取消</button>
495
+ </div>
496
+ </div>
497
+
498
+ <!-- 导入已有会话 -->
499
+ <div id="import-menu">
500
+ <div id="import-backdrop" onclick="closeImportSheet()" style="position:absolute;inset:0;background:rgba(0,0,0,0.55)"></div>
501
+ <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">
502
+ <div class="sheet-handle"></div>
503
+ <div class="sheet-title">导入已有 Claude 会话</div>
504
+ <div id="import-list" style="flex:1;overflow-y:auto;padding:0.4rem 0;-webkit-overflow-scrolling:touch"></div>
505
+ <div style="padding:0.75rem 0;border-top:1px solid var(--border)">
506
+ <button class="dir-btn cancel" onclick="closeImportSheet()" style="width:100%">取消</button>
507
+ </div>
508
+ </div>
509
+ </div>
510
+
511
+ <script>
512
+ // Service Worker 注册
513
+ if ('serviceWorker' in navigator) {
514
+ navigator.serviceWorker.register('/sw.js').catch(() => {});
515
+ }
516
+ const socket = io({
517
+ transports: ['polling', 'websocket'],
518
+ reconnection: true,
519
+ reconnectionAttempts: Infinity,
520
+ reconnectionDelay: 1000,
521
+ reconnectionDelayMax: 10000,
522
+ timeout: 30000,
523
+ });
524
+
525
+ const connDot = document.getElementById('conn-dot');
526
+ const list = document.getElementById('list');
527
+
528
+ // 加载当前用户信息(独占模式显示用户名、退出按钮、锁定目录)
529
+ let _userDir = null;
530
+ fetch('/api/me').then(r => r.json()).then(data => {
531
+ if ((data.exclusive || data.hasUsers) && data.name) {
532
+ document.getElementById('user-name').textContent = data.name;
533
+ document.getElementById('logout-btn').style.display = '';
534
+ }
535
+ if (data.dir) {
536
+ _userDir = data.dir;
537
+ // 默认填入绑定目录,隐藏盘符栏,保留最近目录
538
+ document.getElementById('inp-cwd').value = data.dir;
539
+ document.getElementById('drive-bar').style.display = 'none';
540
+ renderRecentDirs();
541
+ }
542
+ }).catch(() => {});
543
+
544
+ async function showUsage() {
545
+ try {
546
+ const res = await fetch('/api/usage');
547
+ const data = await res.json();
548
+ const C = 'style="margin-bottom:1rem;padding:0.8rem 1rem;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);border-radius:10px"';
549
+ const H = 'style="font-size:0.9rem;font-weight:700;margin-bottom:0.5rem;color:#e6edf3"';
550
+ const T = 'style="font-size:0.82rem;color:#b1bac4;line-height:1.6"';
551
+ let html = '<div style="padding:1.5rem">';
552
+ html += '<h3 style="margin:0 0 1.2rem;font-size:1.1rem;color:#fff;text-align:center">Claude 用量</h3>';
553
+ if (data.rateLimit) {
554
+ const rl = data.rateLimit;
555
+ const resetTime = new Date(rl.resetsAt * 1000);
556
+ const remaining = Math.max(0, Math.ceil((resetTime - Date.now()) / 60000));
557
+ const hours = Math.floor(remaining / 60);
558
+ const mins = remaining % 60;
559
+ const timeStr = hours > 0 ? `${hours}小时${mins}分钟` : `${mins}分钟`;
560
+ const statusColor = rl.status === 'allowed' ? '#3fb950' : '#f85149';
561
+ const statusText = rl.status === 'allowed' ? '正常可用' : '已达上限';
562
+ html += `<div ${C}>`;
563
+ html += `<div ${H}>5 小时配额窗口</div>`;
564
+ html += `<div ${T}><span style="color:${statusColor};font-weight:700;font-size:0.95rem">● ${statusText}</span></div>`;
565
+ html += `<div ${T}>窗口重置: <b style="color:#e6edf3">${timeStr}后</b>(${resetTime.toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit'})})</div>`;
566
+ html += '</div>';
567
+ } else {
568
+ html += `<div ${C}><div ${H}>5 小时配额窗口</div><div ${T}>发送一条 Agent 消息后可查看</div></div>`;
569
+ }
570
+ if (data.activeSessions?.length) {
571
+ html += `<div ${C}><div ${H}>当前会话</div>`;
572
+ for (const s of data.activeSessions) {
573
+ if (!s.usage) continue;
574
+ const u = s.usage;
575
+ html += `<div ${T}><b style="color:#e6edf3">${s.title}</b> (${s.owner || '管理员'})</div>`;
576
+ html += `<div style="font-size:0.78rem;color:#8b949e;margin:0.2rem 0 0.3rem;line-height:1.5">查询 <b style="color:#b1bac4">${u.queryCount}</b>次 输入 <b style="color:#b1bac4">${((u.totalInputTokens+u.totalCacheReadTokens)/1000).toFixed(0)}K</b> 输出 <b style="color:#b1bac4">${(u.totalOutputTokens/1000).toFixed(0)}K</b> 费用 <b style="color:#b1bac4">$${u.totalCostUSD.toFixed(4)}</b></div>`;
577
+ if (u.modelUsage) { for (const [m, mu] of Object.entries(u.modelUsage)) { html += `<div style="font-size:0.72rem;color:#6e7681;line-height:1.4"> ${m.replace(/claude-/,'').replace(/-\d{8}$/,'').replace(/\[.*\]/,'')} ${(mu.inputTokens/1000).toFixed(0)}K/${(mu.outputTokens/1000).toFixed(0)}K $${mu.costUSD.toFixed(4)}</div>`; } }
578
+ }
579
+ html += '</div>';
580
+ }
581
+ html += '<button onclick="this.closest(\'[data-usage-overlay]\').remove()" style="margin-top:0.8rem;width:100%;padding:0.7rem;background:#f97316;color:#fff;border:none;border-radius:10px;cursor:pointer;font-size:0.9rem;font-weight:600">关闭</button></div>';
582
+ const overlay = document.createElement('div');
583
+ overlay.setAttribute('data-usage-overlay', '');
584
+ overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:999;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)';
585
+ overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
586
+ const box = document.createElement('div');
587
+ box.style.cssText = 'background:#161b22;border:1px solid rgba(255,255,255,0.12);border-radius:16px;max-height:80vh;overflow-y:auto;width:90%;max-width:420px;box-shadow:0 16px 48px rgba(0,0,0,0.4)';
588
+ box.innerHTML = html;
589
+ overlay.appendChild(box);
590
+ document.body.appendChild(overlay);
591
+ } catch (err) {
592
+ alert('获取用量失败: ' + err.message);
593
+ }
594
+ }
595
+
596
+ function doLogout() {
597
+ if (!confirm('确定退出登录?')) return;
598
+ fetch('/logout', { method: 'POST' }).finally(() => {
599
+ socket.disconnect();
600
+ location.href = '/login';
601
+ });
602
+ }
603
+
604
+ // HTML 转义,防止 XSS
605
+ function esc(s) {
606
+ const d = document.createElement('div');
607
+ d.textContent = s;
608
+ return d.innerHTML;
609
+ }
610
+
611
+ // ── Connection ──────────────────────────────────────────────
612
+ socket.on('connect', () => {
613
+ connDot.className = 'on';
614
+ socket.emit('list');
615
+ });
616
+ socket.on('disconnect', () => { connDot.className = 'off'; });
617
+ socket.on('sessions', renderSessions);
618
+
619
+ // Refresh periodically
620
+ setInterval(() => { if (socket.connected) socket.emit('list'); }, 5000);
621
+
622
+ // ── 移动端连接保活 ──────────────────────────────
623
+ document.addEventListener('visibilitychange', () => {
624
+ if (!document.hidden) {
625
+ if (!socket.connected) socket.connect();
626
+ else socket.emit('list');
627
+ }
628
+ });
629
+ window.addEventListener('online', () => { if (!socket.connected) socket.connect(); });
630
+ setInterval(() => { if (!document.hidden && !socket.connected) socket.connect(); }, 15000);
631
+
632
+ // ── Render ──────────────────────────────────────────────────
633
+ const PALETTE = ['#f97316','#2563eb','#059669','#b45309','#db2777','#0891b2'];
634
+
635
+ function avatarColor(title) {
636
+ let h = 0;
637
+ for (const c of (title || '?')) h = (h * 31 + c.charCodeAt(0)) & 0xff;
638
+ return PALETTE[h % PALETTE.length];
639
+ }
640
+
641
+ function fmtCwd(cwd) {
642
+ if (!cwd) return '';
643
+ // Show last 2 path segments
644
+ const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean);
645
+ return parts.slice(-2).join('/');
646
+ }
647
+
648
+ function fmtTime(iso) {
649
+ const d = new Date(iso);
650
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
651
+ }
652
+
653
+ function renderSessions(sessions) {
654
+ if (!sessions.length) {
655
+ list.innerHTML = `<div class="empty-state">
656
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
657
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
658
+ <path d="M8 9l4 3-4 3M13 15h3"/>
659
+ </svg>
660
+ <p>还没有终端会话</p>
661
+ </div>`;
662
+ return;
663
+ }
664
+
665
+ list.innerHTML = sessions.map(s => {
666
+ const safeTitle = esc(s.title || '');
667
+ const safeId = esc(s.id);
668
+ const color = avatarColor(s.title);
669
+ const letter = esc((s.title || '?')[0].toUpperCase());
670
+ const sub = esc([fmtCwd(s.cwd), fmtTime(s.createdAt), s.viewers > 0 ? `${s.viewers} 在线` : '', s.controlHolderName ? `${s.controlHolderName} 控制中` : ''].filter(Boolean).join(' · '));
671
+ return `
672
+ <div class="card" data-id="${safeId}" data-title="${safeTitle}">
673
+ <div class="avatar" style="background:${color}">${letter}</div>
674
+ <div class="card-body">
675
+ <div class="card-title">${safeTitle}</div>
676
+ <div class="card-sub">${sub}</div>
677
+ </div>
678
+ <div class="card-right">
679
+ <div class="status-dot ${s.alive ? 'alive' : 'dead'}"></div>
680
+ ${s.viewers > 1 ? `<div class="viewers">👁 ${s.viewers}</div>` : ''}
681
+ </div>
682
+ </div>`;
683
+ }).join('');
684
+
685
+ // 用事件委托替代内联 onclick/oncontextmenu
686
+ list.querySelectorAll('.card').forEach(card => {
687
+ const id = card.dataset.id;
688
+ const title = card.dataset.title;
689
+ card.addEventListener('click', () => openSession(id));
690
+ card.addEventListener('contextmenu', (e) => showKill(e, id, title));
691
+ });
692
+ }
693
+
694
+ // ── Navigation ──────────────────────────────────────────────
695
+ function openSession(id) {
696
+ window.location.href = `/terminal/${id}`;
697
+ }
698
+
699
+ // ── Presets ───────────────────────────────────────────────────
700
+ const PRESETS = [
701
+ { icon: '🛡️', label: '交互审批', title: 'claude', type: 'agent', permissionMode: 'default' },
702
+ { icon: '✏️', label: '自动编辑', title: 'claude', type: 'agent', permissionMode: 'acceptEdits' },
703
+ { icon: '⚡', label: '全自动', title: 'claude', type: 'agent', permissionMode: 'bypassPermissions' },
704
+ ];
705
+
706
+ let selectedPreset = null;
707
+
708
+ function renderPresets() {
709
+ const el = document.getElementById('presets');
710
+ el.innerHTML = PRESETS.map((p, i) => `
711
+ <div class="chip" id="chip-${i}" onclick="selectPreset(${i})">
712
+ <span class="chip-icon">${p.icon}</span>
713
+ <span class="chip-label">${p.label}</span>
714
+ </div>`).join('');
715
+ }
716
+ renderPresets();
717
+
718
+ function selectPreset(i) {
719
+ selectedPreset = i;
720
+ document.querySelectorAll('.chip').forEach((c, j) =>
721
+ c.classList.toggle('selected', j === i));
722
+ const p = PRESETS[i];
723
+ document.getElementById('inp-title').value = p.title;
724
+ document.getElementById('inp-cmd').value = p.cmd || '';
725
+ }
726
+
727
+ // ── Create sheet ─────────────────────────────────────────────
728
+ function openSheet() {
729
+ document.getElementById('sheet-backdrop').classList.add('open');
730
+ document.getElementById('sheet').classList.add('open');
731
+ // Default to first preset
732
+ selectPreset(0);
733
+ setTimeout(() => document.getElementById('inp-title').focus(), 300);
734
+ }
735
+ function closeSheet() {
736
+ document.getElementById('sheet-backdrop').classList.remove('open');
737
+ document.getElementById('sheet').classList.remove('open');
738
+ selectedPreset = null;
739
+ document.querySelectorAll('.chip').forEach(c => c.classList.remove('selected'));
740
+ }
741
+
742
+ document.getElementById('inp-title').addEventListener('keydown', e => {
743
+ if (e.key === 'Enter') createSession();
744
+ });
745
+
746
+ function createSession() {
747
+ const title = document.getElementById('inp-title').value.trim() || 'shell';
748
+ const initCmd = document.getElementById('inp-cmd').value.trim() || undefined;
749
+ const cwd = document.getElementById('inp-cwd').value.trim() || undefined;
750
+ if (cwd) recordDir(cwd);
751
+ const preset = selectedPreset != null ? PRESETS[selectedPreset] : null;
752
+ closeSheet();
753
+ document.getElementById('inp-title').value = '';
754
+ document.getElementById('inp-cmd').value = '';
755
+ document.getElementById('inp-cwd').value = '';
756
+
757
+ if (preset?.type === 'agent') {
758
+ // Agent 模式:使用 Claude SDK
759
+ socket.emit('create-agent', {
760
+ title,
761
+ cwd,
762
+ permissionMode: preset.permissionMode,
763
+ }, (res) => {
764
+ if (res?.ok) openSession(res.session.id);
765
+ else alert('创建失败: ' + (res?.error || '未知错误'));
766
+ });
767
+ } else {
768
+ // PTY 模式:创建终端会话
769
+ socket.emit('create', { title, initCmd, cwd }, (res) => {
770
+ if (res?.ok) openSession(res.session.id);
771
+ else alert('创建失败: ' + (res?.error || '未知错误'));
772
+ });
773
+ }
774
+ }
775
+
776
+ // ── Dir picker ───────────────────────────────────────────────
777
+ let _dirCurrent = '';
778
+ const RECENT_DIRS_KEY = 'zihi_recent_dirs'; // { path: count } 频次记录
779
+ const RECENT_DIRS_MAX = 5;
780
+
781
+ function getRecentDirs() {
782
+ try { return JSON.parse(localStorage.getItem(RECENT_DIRS_KEY)) || {}; } catch { return {}; }
783
+ }
784
+ function recordDir(dir) {
785
+ if (!dir) return;
786
+ const dirs = getRecentDirs();
787
+ dirs[dir] = (dirs[dir] || 0) + 1;
788
+ // 只保留使用最多的若干条
789
+ const sorted = Object.entries(dirs).sort((a, b) => b[1] - a[1]);
790
+ const trimmed = Object.fromEntries(sorted.slice(0, RECENT_DIRS_MAX));
791
+ localStorage.setItem(RECENT_DIRS_KEY, JSON.stringify(trimmed));
792
+ renderRecentDirs();
793
+ }
794
+ function renderRecentDirs() {
795
+ const container = document.getElementById('recent-dirs');
796
+ container.innerHTML = '';
797
+ const dirs = getRecentDirs();
798
+ const sorted = Object.entries(dirs).sort((a, b) => b[1] - a[1]);
799
+ const filtered = _userDir ? sorted.filter(([path]) => path.startsWith(_userDir)) : sorted;
800
+ if (!filtered.length) return;
801
+ filtered.forEach(([path]) => {
802
+ const btn = document.createElement('button');
803
+ // 只显示最后一级目录名
804
+ const short = path.replace(/[\\/]$/, '').split(/[\\/]/).pop() || path;
805
+ btn.textContent = short;
806
+ btn.title = path;
807
+ 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';
808
+ btn.onclick = () => { document.getElementById('inp-cwd').value = path; };
809
+ container.appendChild(btn);
810
+ });
811
+ }
812
+ renderRecentDirs();
813
+
814
+ function openDirPicker() {
815
+ document.getElementById('dir-picker').classList.add('open');
816
+ const dirs = getRecentDirs();
817
+ const topDir = Object.entries(dirs).sort((a, b) => b[1] - a[1])[0]?.[0] || '';
818
+ const initial = document.getElementById('inp-cwd').value.trim() || topDir || '';
819
+ loadDirs(initial);
820
+ loadDrives();
821
+ }
822
+ function closeDirPicker() {
823
+ document.getElementById('dir-picker').classList.remove('open');
824
+ }
825
+ function selectDir() {
826
+ document.getElementById('inp-cwd').value = _dirCurrent;
827
+ recordDir(_dirCurrent);
828
+ closeDirPicker();
829
+ }
830
+
831
+ // Windows 盘符检测和切换
832
+ function loadDrives() {
833
+ const bar = document.getElementById('drive-bar');
834
+ if (!bar) return;
835
+ // 通过尝试访问常见盘符来检测
836
+ const letters = ['C', 'D', 'E', 'F', 'G', 'H'];
837
+ bar.innerHTML = '';
838
+ let found = 0;
839
+ for (const letter of letters) {
840
+ const path = letter + ':\\';
841
+ socket.emit('dirs', path, (data) => {
842
+ if (data.ok) {
843
+ found++;
844
+ const btn = document.createElement('button');
845
+ btn.className = 'drive-btn';
846
+ btn.textContent = letter + ':';
847
+ btn.onclick = () => {
848
+ loadDirs(path);
849
+ bar.querySelectorAll('.drive-btn').forEach(b => b.classList.remove('active'));
850
+ btn.classList.add('active');
851
+ };
852
+ // 高亮当前盘符
853
+ if (_dirCurrent && _dirCurrent.toUpperCase().startsWith(letter + ':')) {
854
+ btn.classList.add('active');
855
+ }
856
+ bar.appendChild(btn);
857
+ }
858
+ });
859
+ }
860
+ }
861
+
862
+ function loadDirs(path) {
863
+ document.getElementById('dir-list').innerHTML = '<div style="padding:1rem;color:var(--muted);font-size:0.85rem">加载中...</div>';
864
+ socket.emit('dirs', path || '', (data) => {
865
+ if (!data.ok) {
866
+ document.getElementById('dir-list').innerHTML = `<div style="padding:1rem;color:var(--red);font-size:0.85rem">${escH(data.error)}</div>`;
867
+ return;
868
+ }
869
+ _dirCurrent = data.current;
870
+ document.getElementById('dir-current').textContent = data.current;
871
+ // 更新盘符高亮
872
+ document.querySelectorAll('.drive-btn').forEach(btn => {
873
+ btn.classList.toggle('active', _dirCurrent.toUpperCase().startsWith(btn.textContent.toUpperCase()));
874
+ });
875
+ const dirList = document.getElementById('dir-list');
876
+ dirList.innerHTML = '';
877
+ if (data.parent) {
878
+ const el = document.createElement('div');
879
+ el.className = 'dir-item';
880
+ el.dataset.path = data.parent;
881
+ el.innerHTML = '<span class="di">↩</span><span class="dn">..</span>';
882
+ dirList.appendChild(el);
883
+ }
884
+ for (const d of data.dirs) {
885
+ const el = document.createElement('div');
886
+ el.className = 'dir-item';
887
+ el.dataset.path = d.path;
888
+ el.innerHTML = `<span class="di">📁</span><span class="dn">${escH(d.name)}</span>`;
889
+ dirList.appendChild(el);
890
+ }
891
+ if (!dirList.children.length) {
892
+ dirList.innerHTML = '<div style="padding:1rem;color:var(--muted);font-size:0.85rem">无子目录</div>';
893
+ }
894
+ });
895
+ }
896
+ document.getElementById('dir-list').addEventListener('click', e => {
897
+ const item = e.target.closest('.dir-item');
898
+ if (item?.dataset.path) loadDirs(item.dataset.path);
899
+ });
900
+ function escH(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
901
+
902
+ // ── 导入已有 Claude 会话 ───────────────────────────────────
903
+ function openImportSheet() {
904
+ closeSheet(); // 关闭创建面板
905
+ document.getElementById('import-menu').classList.add('open');
906
+ document.getElementById('import-list').innerHTML = '<div style="padding:1rem;color:var(--muted);font-size:0.85rem">加载中...</div>';
907
+ // 通过 REST API 获取本地 Claude 会话列表
908
+ fetch('/api/claude-sessions')
909
+ .then(r => r.json())
910
+ .then(sessions => {
911
+ const el = document.getElementById('import-list');
912
+ if (!sessions.length) {
913
+ 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>';
914
+ return;
915
+ }
916
+ el.innerHTML = '';
917
+ for (const s of sessions) {
918
+ const item = document.createElement('div');
919
+ item.className = 'import-item';
920
+ item.onclick = () => importSession(s);
921
+ const time = s.updatedAt ? new Date(s.updatedAt).toLocaleString('zh-CN', { month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit' }) : '';
922
+ const proj = s.projectDir ? decodeURIComponent(s.projectDir).split(/[\\/]/).pop() : '';
923
+ const name = s.summary || s.sessionId.slice(0, 12);
924
+ item.innerHTML = `
925
+ <span class="ii-icon">🤖</span>
926
+ <div class="ii-body">
927
+ <div class="ii-id">${escH(name)}</div>
928
+ <div class="ii-info">${time ? time + ' · ' : ''}${s.messageCount} 条消息${proj ? ' · ' + escH(proj) : ''}</div>
929
+ </div>`;
930
+ el.appendChild(item);
931
+ }
932
+ })
933
+ .catch(err => {
934
+ document.getElementById('import-list').innerHTML = `<div style="padding:1rem;color:var(--red);font-size:0.85rem">加载失败: ${escH(err.message)}</div>`;
935
+ });
936
+ }
937
+
938
+ function closeImportSheet() {
939
+ document.getElementById('import-menu').classList.remove('open');
940
+ }
941
+
942
+ function importSession(session) {
943
+ closeImportSheet();
944
+ const cwd = session.projectPath || document.getElementById('inp-cwd').value.trim() || undefined;
945
+ const title = session.summary ? session.summary.slice(0, 30) : 'claude (导入)';
946
+ socket.emit('create-agent', {
947
+ title,
948
+ cwd,
949
+ resumeSessionId: session.sessionId,
950
+ }, (res) => {
951
+ if (res?.ok) openSession(res.session.id);
952
+ else alert('导入失败: ' + (res?.error || '未知错误'));
953
+ });
954
+ }
955
+
956
+ // ── Kill ─────────────────────────────────────────────────────
957
+ let _killId = null;
958
+ function showKill(e, id, title) {
959
+ e.preventDefault();
960
+ _killId = id;
961
+ document.getElementById('kill-title').textContent = title;
962
+ document.getElementById('kill-menu').classList.add('open');
963
+ }
964
+ function closeKill() {
965
+ document.getElementById('kill-menu').classList.remove('open');
966
+ _killId = null;
967
+ }
968
+ function confirmKill() {
969
+ if (_killId) socket.emit('kill', _killId);
970
+ closeKill();
971
+ }
972
+
973
+ // ── 版本更新 ──────────────────────────────────────────────
974
+ </script>
975
+ </body>
976
+ </html>