@doubleelec/dsh-workspace-explorer 0.7.1-fork.4

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,1129 @@
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" />
6
+ <title>dsh-workspace-explorer · Interactive Demo</title>
7
+ <meta name="description" content="Interactive mock preview of the dsh-workspace-explorer plugin for DeepSeek Harness: session-header entry, split-view preview / edit panel, workspace file tree with click / drag / multi-select references, folder tree insertion and paginated preview." />
8
+ <style>
9
+ * { box-sizing: border-box; }
10
+ html, body { height: 100%; }
11
+ body {
12
+ margin: 0;
13
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
14
+ color: #e8e8e8;
15
+ background: radial-gradient(1200px 600px at 20% -10%, #1c2430 0%, #0d1117 55%, #080b0f 100%);
16
+ display: flex; flex-direction: column; align-items: center;
17
+ padding: 24px 16px 40px;
18
+ }
19
+
20
+ /* ---- demo 说明条 ---- */
21
+ .demo-note {
22
+ max-width: 960px; width: 100%;
23
+ background: rgba(255,255,255,.05);
24
+ border: 1px solid rgba(255,255,255,.08);
25
+ border-radius: 12px;
26
+ padding: 10px 16px;
27
+ margin-bottom: 14px;
28
+ font-size: 12.5px;
29
+ color: #aeb6c2;
30
+ display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center;
31
+ }
32
+ .demo-note b { color: #e8e8e8; }
33
+ .demo-note a { color: #6ea8fe; text-decoration: none; }
34
+ .demo-note a:hover { text-decoration: underline; }
35
+ .demo-note .pill {
36
+ margin-left: auto; flex: none;
37
+ background: rgba(65,118,230,.16); color: #9ec5ff;
38
+ border: 1px solid rgba(65,118,230,.35);
39
+ border-radius: 999px; padding: 2px 10px; font-size: 11.5px;
40
+ }
41
+ .demo-lang {
42
+ flex: none; cursor: pointer; border: 1px solid rgba(255,255,255,.2); background: transparent;
43
+ color: #cfd6e0; border-radius: 999px; padding: 2px 10px; font-size: 11.5px;
44
+ }
45
+ .demo-lang:hover { border-color: #6ea8fe; color: #fff; }
46
+
47
+ /* ---- 舞台:模拟 DSH 窗口 ---- */
48
+ .demo-stage {
49
+ position: relative;
50
+ width: min(960px, 100%);
51
+ height: min(620px, calc(100dvh - 130px));
52
+ min-height: 440px;
53
+ background: #15171a;
54
+ border: 1px solid rgba(255,255,255,.09);
55
+ border-radius: 18px;
56
+ overflow: hidden;
57
+ box-shadow: 0 24px 70px rgba(0,0,0,.55);
58
+ }
59
+
60
+ /* ---- 会话头部(面包屑 + 工具区) ---- */
61
+ .demo-header {
62
+ position: absolute; top: 0; left: 0; right: 0; z-index: 5;
63
+ display: flex; align-items: center; gap: 12px;
64
+ padding: 8px 16px;
65
+ border-bottom: 1px solid rgba(255,255,255,.07);
66
+ background: rgba(21,23,26,.92);
67
+ }
68
+ .demo-header-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
69
+ .demo-header-left .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; flex: none; }
70
+ .demo-crumbs { display: flex; align-items: center; gap: 4px; font-size: 12.5px; min-width: 0; overflow: hidden; }
71
+ .demo-crumbs .crumb { color: #9aa3b0; background: transparent; border: 0; padding: 2px 4px; border-radius: 6px; font: inherit; }
72
+ .demo-crumbs .crumb-sep { color: #565e69; }
73
+ .demo-crumbs .crumb-current { color: #e8e8e8; font-weight: 600; }
74
+ .demo-header-utils { display: flex; align-items: center; gap: 8px; flex: none; }
75
+
76
+ /* Session log 胶囊(DSH 原生) */
77
+ .demo-sessionlog {
78
+ border: 1px solid rgba(255,255,255,.16);
79
+ min-width: 96px; height: 32px;
80
+ color: #e8e8e8; font-family: inherit; cursor: default;
81
+ background: transparent; border-radius: 18px;
82
+ justify-content: center; align-items: center; gap: 4px;
83
+ padding: 6px 12px; font-size: 13px; font-weight: 400; line-height: 20px;
84
+ display: inline-flex; flex: none; opacity: .85;
85
+ }
86
+ .demo-sessionlog span, .demo-sessionlog svg { flex: none; }
87
+ .demo-sessionlog span { white-space: nowrap; }
88
+ .demo-sessionlog:hover { background: rgba(255,255,255,.07); }
89
+
90
+ /* 插件入口胶囊:功能名称 + 图标(与真实插件一致) */
91
+ .demo-stage .dshwe-hicon {
92
+ border: 1px solid rgba(255,255,255,.16);
93
+ height: 32px;
94
+ color: #e8e8e8;
95
+ cursor: pointer;
96
+ background: transparent;
97
+ border-radius: 18px;
98
+ justify-content: center;
99
+ align-items: center;
100
+ gap: 4px;
101
+ padding: 6px 12px;
102
+ font-size: 13px;
103
+ font-weight: 400;
104
+ line-height: 20px;
105
+ display: inline-flex;
106
+ flex: none;
107
+ }
108
+ .demo-stage .dshwe-hicon span, .demo-stage .dshwe-hicon svg { flex: none; }
109
+ .demo-stage .dshwe-hicon span { white-space: nowrap; }
110
+ .demo-stage .dshwe-hicon:hover { background: rgba(255,255,255,.07); }
111
+ .demo-stage .dshwe-hicon-on { color: #6ea8fe; border-color: rgba(110,168,254,.55); }
112
+
113
+ /* ---- 左侧:假聊天区 ---- */
114
+ .demo-chat { position: absolute; inset: 0; display: flex; flex-direction: column; padding-top: 49px; transition: padding-right .25s ease; }
115
+ .demo-stage.panel-open .demo-chat { padding-right: 404px; }
116
+ .demo-stage.panel-open.preview-open .demo-chat { padding-right: 744px; }
117
+ .demo-log { flex: 1; overflow: auto; padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; }
118
+ .demo-log-empty { color: #5d646e; font-size: 12.5px; }
119
+ .demo-msg {
120
+ align-self: flex-end; max-width: 82%;
121
+ background: #1e242c; border: 1px solid rgba(255,255,255,.08);
122
+ border-radius: 12px 12px 4px 12px; padding: 8px 12px;
123
+ font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
124
+ color: #c8d0db; word-break: break-all; white-space: pre-wrap;
125
+ }
126
+ .demo-msg .tag { color: #6ea8fe; }
127
+ .demo-composer { flex: none; padding: 10px 14px 14px; border-top: 1px solid rgba(255,255,255,.07); background: rgba(255,255,255,.02); }
128
+ .demo-composer textarea {
129
+ width: 100%; height: 64px; resize: none;
130
+ background: #0f1215; color: #e8e8e8;
131
+ border: 1px solid rgba(255,255,255,.12); border-radius: 12px;
132
+ padding: 10px 12px; font: inherit; font-size: 13px; outline: none;
133
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px;
134
+ }
135
+ .demo-composer textarea:focus { border-color: #4176e6; }
136
+ .demo-composer-bar { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
137
+ .demo-send {
138
+ margin-left: auto;
139
+ background: #4176e6; color: #fff; border: 0; border-radius: 8px;
140
+ padding: 6px 16px; font: inherit; font-size: 12.5px; cursor: pointer;
141
+ }
142
+ .demo-send:hover { background: #4c83f5; }
143
+
144
+ /* ---- 插件弹窗(与真实插件一致:Tab 栏 + 文件/设置页) ---- */
145
+ .demo-stage .dshwe-layer { position: absolute; inset: 0; z-index: 100; pointer-events: none; }
146
+ .dshwe-popup {
147
+ position: absolute; right: 14px; top: 61px; bottom: 14px;
148
+ width: 384px;
149
+ display: flex; flex-direction: column;
150
+ background: #26282c;
151
+ border: 1px solid rgba(128,128,128,.28);
152
+ border-radius: 16px;
153
+ box-shadow: 0 12px 32px rgba(0,0,0,.5);
154
+ overflow: hidden;
155
+ color: #e8e8e8;
156
+ pointer-events: auto;
157
+ opacity: 0;
158
+ transform: translateY(-8px) scale(.98);
159
+ transform-origin: top right;
160
+ transition: opacity .18s ease, transform .18s ease;
161
+ }
162
+ .dshwe-popup-on { opacity: 1; transform: translateY(0) scale(1); }
163
+ @media (prefers-reduced-motion: reduce) { .dshwe-popup { transition: none } }
164
+
165
+ .dshwe-tabs { display: flex; gap: 2px; padding: 0 14px; flex: none; border-bottom: 1px solid rgba(255,255,255,.12); }
166
+ .dshwe-tab {
167
+ flex: 1; position: relative; height: 36px;
168
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
169
+ border: 0; background: transparent; color: #9a9a9a;
170
+ font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer;
171
+ border-radius: 10px 10px 0 0;
172
+ }
173
+ .dshwe-tab:hover { color: #e8e8e8; background: rgba(255,255,255,.05); }
174
+ .dshwe-tab-on { color: #e8e8e8; }
175
+ .dshwe-tab-ind {
176
+ position: absolute; left: 24%; right: 24%; bottom: -1px; height: 2px; border-radius: 999px;
177
+ background: #4176e6;
178
+ opacity: 0; transform: scaleX(.4);
179
+ transition: opacity .16s ease, transform .16s ease;
180
+ }
181
+ .dshwe-tab-on .dshwe-tab-ind { opacity: 1; transform: scaleX(1); }
182
+ .dshwe-tabbody { display: flex; flex-direction: column; flex: 1; min-height: 0; }
183
+ .dshwe-popup[hidden], .dshwe-tabbody[hidden], .dshwe-selbar[hidden], .dshwe-preview-slot[hidden], .dshwe-hint[hidden] { display: none !important; }
184
+
185
+ .dshwe-selrow { display: flex; gap: 8px; padding: 10px 16px 6px; align-items: center; }
186
+ .dshwe-sel { flex: 1; min-width: 0; height: 30px; background: transparent; color: #e8e8e8; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; padding: 0 8px; font: inherit; font-size: 12.5px; outline: none; }
187
+ .dshwe-sel:focus { border-color: #4176e6; }
188
+ .dshwe-sel option { background: #1c1e22; }
189
+ .dshwe-filterrow { display: flex; gap: 6px; padding: 0 16px 6px; align-items: center; }
190
+ .dshwe-filter { flex: 1; min-width: 0; height: 30px; background: transparent; color: #e8e8e8; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; padding: 0 8px; font: inherit; font-size: 12.5px; outline: none; }
191
+ .dshwe-filter:focus { border-color: #4176e6; }
192
+ .dshwe-filter::placeholder { color: #8a8a8a; }
193
+ .dshwe-filter-clear { flex: none; border: 0; background: transparent; color: #9a9a9a; width: 26px; height: 26px; border-radius: 6px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
194
+ .dshwe-filter-clear:hover { color: #e8e8e8; background: rgba(255,255,255,.08); }
195
+ .dshwe-icobtn { border: 0; background: transparent; color: #9a9a9a; width: 28px; height: 28px; border-radius: 8px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; flex: none; }
196
+ .dshwe-icobtn:hover { background: rgba(255,255,255,.08); color: #e8e8e8; }
197
+ .dshwe-hintline { display: flex; align-items: center; gap: 6px; padding: 0 18px 8px; color: #8a8a8a; font-size: 11px; }
198
+ .dshwe-tree { flex: 1; overflow: auto; padding: 2px 6px 10px; }
199
+ .dshwe-tree-note { padding: 6px 16px 2px; color: #8a8a8a; font-size: 11px; }
200
+
201
+ .dshwe-row { display: flex; align-items: center; gap: 7px; padding: 4.5px 10px; cursor: pointer; user-select: none; white-space: nowrap; border: 0; width: 100%; text-align: left; background: transparent; color: inherit; font: inherit; border-radius: 10px; }
202
+ .dshwe-row:hover { background: rgba(255,255,255,.08); }
203
+ .dshwe-row-file { cursor: grab; }
204
+ .dshwe-row-file:active { cursor: grabbing; }
205
+ .dshwe-chev-slot { width: 14px; flex: none; display: inline-flex; align-items: center; justify-content: center; }
206
+ .dshwe-chev { color: #777; transition: transform .16s ease; display: inline-flex; }
207
+ .dshwe-chev-on { transform: rotate(90deg); }
208
+ .dshwe-ico { flex: none; display: inline-flex; align-items: center; justify-content: center; }
209
+ .dshwe-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 12.5px; }
210
+ .dshwe-row-dir .dshwe-name { font-weight: 600; }
211
+ .dshwe-row-sel { background: rgba(65,118,230,.16); box-shadow: inset 2px 0 0 #4176e6; }
212
+ .dshwe-row-sel:hover { background: rgba(65,118,230,.24); }
213
+ .dshwe-size { flex: none; color: #8a8a8a; font-size: 11px; font-variant-numeric: tabular-nums; padding-left: 6px; }
214
+
215
+ .dshwe-selbar { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-top: 1px solid rgba(255,255,255,.12); background: #222426; flex: none; }
216
+ .dshwe-selbar-count { flex: 1; min-width: 0; font-size: 12px; color: #e8e8e8; font-variant-numeric: tabular-nums; }
217
+ .dshwe-prevbtn { flex: none; border: 1px solid rgba(255,255,255,.16); background: transparent; color: #9a9a9a; border-radius: 8px; padding: 4px 10px; font: inherit; font-size: 12px; cursor: pointer; }
218
+ .dshwe-prevbtn:hover { color: #e8e8e8; background: rgba(255,255,255,.08); }
219
+ .dshwe-prevbtn-primary { color: #fff; background: #4176e6; border-color: transparent; }
220
+ .dshwe-prevbtn-primary:hover { opacity: .9; background: #4176e6; }
221
+ .dshwe-pager-btn { flex: none; width: 24px; height: 24px; border: 1px solid rgba(255,255,255,.16); background: transparent; color: #9a9a9a; border-radius: 7px; font: inherit; font-size: 14px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
222
+ .dshwe-pager-btn:hover:not(:disabled) { color: #e8e8e8; background: rgba(255,255,255,.08); }
223
+ .dshwe-pager-btn:disabled { opacity: .4; cursor: not-allowed; }
224
+
225
+ /* ---- 文件行预览按钮(👁,常显;与真实插件一致) ---- */
226
+ .dshwe-preview-btn {
227
+ flex: none; display: inline-flex; align-items: center; justify-content: center;
228
+ width: 26px; height: 26px; border: 0; border-radius: 6px; cursor: pointer;
229
+ background: transparent; color: #9a9a9a;
230
+ transition: background .12s ease, color .12s ease;
231
+ }
232
+ .dshwe-preview-btn:hover { background: rgba(255,255,255,.08); color: #6ea8fe; }
233
+ .dshwe-preview-btn-on { color: #6ea8fe; }
234
+ .dshwe-row-active { background: rgba(65,118,230,.08); }
235
+ .dshwe-row-active:hover { background: rgba(65,118,230,.14); }
236
+ .dshwe-row-preview-dot { width: 6px; height: 6px; border-radius: 50%; background: #4176e6; flex: none; margin-left: 4px; }
237
+
238
+ /* ---- Split view:左侧预览/编辑面板 + 右侧文件树 ---- */
239
+ .dshwe-body { display: flex; flex: 1; min-height: 0; overflow: hidden; }
240
+ .dshwe-tree-col { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
241
+ .dshwe-preview-slot {
242
+ flex: none; width: 340px; display: flex; flex-direction: column;
243
+ border-right: 1px solid rgba(255,255,255,.12);
244
+ background: #222426;
245
+ animation: dshwe-slide-in .2s ease;
246
+ }
247
+ @keyframes dshwe-slide-in { from { width: 0; opacity: 0 } to { width: 340px; opacity: 1 } }
248
+ @media (prefers-reduced-motion: reduce) { .dshwe-preview-slot { animation: none } }
249
+
250
+ .dshwe-preview-panel { display: flex; flex-direction: column; flex: 1; min-width: 0; overflow: hidden; }
251
+ .dshwe-preview-head { display: flex; align-items: center; gap: 6px; padding: 10px 12px 6px; flex: none; flex-wrap: wrap; }
252
+ .dshwe-preview-name { font-size: 12.5px; font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
253
+ .dshwe-preview-meta { font-size: 11px; color: #8a8a8a; flex: none; }
254
+ .dshwe-preview-pre { flex: 1; overflow: auto; margin: 0; padding: 4px 12px 10px; font: 11.5px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #c8d0db; white-space: pre; }
255
+ .dshwe-preview-actions { display: flex; align-items: center; gap: 8px; padding: 0 12px 10px; flex: none; }
256
+ .dshwe-editor {
257
+ flex: 1; resize: none; border: 0; outline: none; padding: 4px 12px 10px;
258
+ background: transparent; color: #c8d0db;
259
+ font: 11.5px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
260
+ white-space: pre; overflow: auto; tab-size: 2;
261
+ }
262
+ .dshwe-editor:focus { outline: none; }
263
+ .dshwe-edit-saving { font-size: 11px; color: #8a8a8a; }
264
+ .dshwe-edit-saveerr { font-size: 11px; color: #e5484d; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
265
+ .dshwe-prevbtn { flex: none; height: 28px; border: 1px solid rgba(255,255,255,.16); background: transparent; color: #9a9a9a; border-radius: 8px; padding: 0 10px; font: inherit; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
266
+ .dshwe-prevbtn:hover { color: #e8e8e8; background: rgba(255,255,255,.08); }
267
+ .dshwe-prevbtn:disabled { opacity: .45; cursor: not-allowed; }
268
+
269
+ /* ---- 设置页 ---- */
270
+ .dshwe-set { display: flex; flex-direction: column; gap: 2px; padding: 8px 16px 14px; overflow: auto; flex: 1; }
271
+ .dshwe-setsec { font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: #8a8a8a; padding: 10px 4px 6px; }
272
+ .dshwe-setrow { display: flex; align-items: center; gap: 12px; padding: 9px 10px; border-radius: 12px; }
273
+ .dshwe-setrow:hover { background: rgba(255,255,255,.05); }
274
+ .dshwe-setinfo { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
275
+ .dshwe-setlabel { font-size: 12.5px; color: #e8e8e8; }
276
+ .dshwe-setcap { font-size: 11px; color: #8a8a8a; }
277
+ .dshwe-setselect {
278
+ flex: none; height: 28px; min-width: 118px;
279
+ background: transparent; color: #e8e8e8;
280
+ border: 1px solid rgba(255,255,255,.16);
281
+ border-radius: 8px; padding: 0 8px; font: inherit; font-size: 12px; outline: none;
282
+ }
283
+ .dshwe-setselect option { background: #1c1e22; }
284
+ .dshwe-switch {
285
+ flex: none; width: 34px; height: 20px; border-radius: 999px; padding: 0;
286
+ border: 1px solid rgba(255,255,255,.2);
287
+ background: rgba(255,255,255,.12);
288
+ position: relative; cursor: pointer; transition: background .15s ease, border-color .15s ease;
289
+ }
290
+ .dshwe-switch::after {
291
+ content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%;
292
+ background: #9a9a9a;
293
+ transition: transform .15s ease;
294
+ }
295
+ .dshwe-switch[aria-checked='true'] { background: #4176e6; border-color: transparent; }
296
+ .dshwe-switch[aria-checked='true']::after { transform: translateX(14px); background: #fff; }
297
+ .dshwe-setnote { font-size: 11px; color: #777; padding: 2px 10px 8px; }
298
+ .dshwe-setfoot { display: flex; justify-content: flex-end; padding: 8px 4px 2px; }
299
+
300
+ /* ---- 拖拽提示 ---- */
301
+ .dshwe-hint {
302
+ position: absolute; inset: 10px; z-index: 200;
303
+ border: 1.5px dashed #4176e6; border-radius: 20px;
304
+ display: flex; align-items: center; justify-content: center; pointer-events: none;
305
+ background: rgba(13,17,23,.35);
306
+ }
307
+ .dshwe-hint-chip { display: flex; align-items: center; gap: 7px; background: #26282c; color: #e8e8e8; padding: 9px 15px; border-radius: 999px; font-size: 13px; box-shadow: 0 4px 12px rgba(0,0,0,.4); }
308
+ .dshwe-hint-chip svg { color: #4176e6; }
309
+ .demo-send:focus-visible, .dshwe-icobtn:focus-visible, .dshwe-act:focus-visible { outline: 2px solid #4176e6; outline-offset: -2px; }
310
+ @media (prefers-reduced-motion: reduce) {
311
+ .demo-chat { transition: none; }
312
+ .dshwe-chev { transition: none; }
313
+ .dshwe-tab-ind { transition: none; }
314
+ .dshwe-popup { transition: none; }
315
+ }
316
+ </style>
317
+ </head>
318
+ <body>
319
+
320
+ <div class="demo-note">
321
+ <b>dsh-workspace-explorer · Interactive Demo</b>
322
+ <span>模拟预览 — 与真实插件 UI 一致(会话头部入口 + 文件树面板 + 分屏预览/编辑 + 多选/拖拽/分页)</span>
323
+ <span>Mock preview of the DeepSeek Harness plugin — header entry, file tree, split-view preview / edit, multi-select / drag / paging.</span>
324
+ <span class="pill">Mock · 非真实运行环境</span>
325
+ <button type="button" class="demo-lang" id="langBtn">English</button>
326
+ <a href="https://github.com/Jiyr0119/dsh-workspace-explorer" target="_blank" rel="noopener">GitHub ↗</a>
327
+ </div>
328
+
329
+ <div class="demo-stage" id="stage">
330
+ <!-- 会话头部:面包屑 + 工具区(与 DSH 一致) -->
331
+ <div class="demo-header">
332
+ <div class="demo-header-left">
333
+ <span class="dot"></span>
334
+ <nav class="demo-crumbs" aria-label="会话层级">
335
+ <span class="crumb" id="crumbWs">workspace</span>
336
+ <span class="crumb-sep">/</span>
337
+ <span class="crumb crumb-current" id="crumbCur">my-app</span>
338
+ </nav>
339
+ </div>
340
+ <div class="demo-header-utils">
341
+ <button type="button" class="demo-sessionlog" id="sessionLogBtn" title="Session log · 演示无操作">
342
+ <span>Session log</span>
343
+ <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M8 2.5v7.5M5.7 7.5L8 9.8l2.3-2.3M3.5 12.5h9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
344
+ </button>
345
+ <button type="button" class="dshwe-hicon" id="toggleBtn" title="文件目录">
346
+ <span id="toggleLabel">工作区文件</span>
347
+ <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M1.5 2.5A1.5 1.5 0 0 1 3 1h3.2l1.6 2H13a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 13 13H3a1.5 1.5 0 0 1-1.5-1.5v-9z" fill="currentColor"/></svg>
348
+ </button>
349
+ </div>
350
+ </div>
351
+
352
+ <!-- 聊天区 -->
353
+ <div class="demo-chat">
354
+ <div class="demo-log" id="log"><div class="demo-log-empty" id="logEmpty">发送文件引用后,这里会出现 `[file: …]` 消息(模拟)。</div></div>
355
+ <div class="demo-composer" id="composerCard">
356
+ <textarea id="composer" placeholder="在此输入… 或点击/拖拽右侧文件"></textarea>
357
+ <div class="demo-composer-bar">
358
+ <button type="button" class="demo-send" id="sendBtn">发送 Send</button>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <!-- 插件弹窗层 -->
364
+ <div class="dshwe-layer" id="layer">
365
+ <div class="dshwe-hint" id="dropHint" hidden>
366
+ <div class="dshwe-hint-chip">
367
+ <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M8 3.5v6M5.7 7.2L8 9.5l2.3-2.3M3.5 12.5h9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
368
+ <span id="dropHintText">松开以插入文件引用到输入框</span>
369
+ </div>
370
+ </div>
371
+
372
+ <div class="dshwe-popup" id="popup" hidden>
373
+ <div class="dshwe-tabs" role="tablist">
374
+ <button type="button" role="tab" class="dshwe-tab dshwe-tab-on" data-tab="files" id="tabFilesBtn">
375
+ <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M1.5 2.5A1.5 1.5 0 0 1 3 1h3.2l1.6 2H13a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 13 13H3a1.5 1.5 0 0 1-1.5-1.5v-9z" fill="currentColor"/></svg>
376
+ <span id="tabFilesLabel">文件</span><span class="dshwe-tab-ind"></span>
377
+ </button>
378
+ <button type="button" role="tab" class="dshwe-tab" data-tab="settings" id="tabSettingsBtn">
379
+ <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M8 9.9a1.9 1.9 0 1 1 0-3.8 1.9 1.9 0 0 1 0 3.8zM8 4.3V2.9M8 13.1v-1.4M4.3 8H2.9M13.1 8h-1.4M5.2 5.2L4.1 4.1M11.9 11.9l-1.1-1.1M5.2 10.8L4.1 11.9M11.9 4.1l-1.1 1.1" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
380
+ <span id="tabSettingsLabel">设置</span><span class="dshwe-tab-ind"></span>
381
+ </button>
382
+ </div>
383
+
384
+ <!-- 文件页 -->
385
+ <div class="dshwe-tabbody" id="tabFiles">
386
+ <div class="dshwe-body">
387
+ <!-- 左侧:分屏预览 / 编辑面板(打开时 340px 撑开,文件树保持可见) -->
388
+ <div class="dshwe-preview-slot" id="previewSlot" hidden>
389
+ <div class="dshwe-preview-panel" id="preview">
390
+ <div class="dshwe-preview-head">
391
+ <span class="dshwe-preview-name" id="previewName"></span>
392
+ <span class="dshwe-preview-meta" id="previewMeta"></span>
393
+ <span class="dshwe-edit-saving" id="previewSaving" hidden></span>
394
+ <span class="dshwe-edit-saveerr" id="previewSaveErr" hidden></span>
395
+ <span id="previewModeBtns">
396
+ <button type="button" class="dshwe-pager-btn" id="prevPage" title="上一页">‹</button>
397
+ <button type="button" class="dshwe-pager-btn" id="nextPage" title="下一页">›</button>
398
+ <button type="button" class="dshwe-prevbtn" id="previewEdit">编辑</button>
399
+ <button type="button" class="dshwe-icobtn" id="previewClose" title="关闭预览">
400
+ <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /></svg>
401
+ </button>
402
+ </span>
403
+ <span id="editModeBtns" hidden>
404
+ <button type="button" class="dshwe-prevbtn" id="editSave">保存</button>
405
+ <button type="button" class="dshwe-prevbtn" id="editDiscard">放弃</button>
406
+ <button type="button" class="dshwe-icobtn" id="editCancel" title="取消">
407
+ <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /></svg>
408
+ </button>
409
+ </span>
410
+ </div>
411
+ <pre class="dshwe-preview-pre" id="previewPre" hidden></pre>
412
+ <textarea class="dshwe-editor" id="previewEditor" hidden spellcheck="false"></textarea>
413
+ <div class="dshwe-preview-actions" id="previewActions">
414
+ <button type="button" class="dshwe-prevbtn" id="previewRef">插入引用</button>
415
+ <button type="button" class="dshwe-prevbtn" id="previewContent">插入内容</button>
416
+ </div>
417
+ </div>
418
+ </div>
419
+ <!-- 右侧:文件树(预览打开时仍可导航) -->
420
+ <div class="dshwe-tree-col">
421
+ <div class="dshwe-selrow">
422
+ <select class="dshwe-sel" id="wsSel"></select>
423
+ <button type="button" class="dshwe-icobtn" id="addBtn" title="添加工作区(演示)">+</button>
424
+ </div>
425
+ <div class="dshwe-filterrow">
426
+ <input class="dshwe-filter" id="filter" placeholder="搜索文件…" />
427
+ <button type="button" class="dshwe-filter-clear" id="filterClear" title="清除" hidden>×</button>
428
+ </div>
429
+ <div class="dshwe-hintline"><span>↩</span><span id="hintText">点击插入;Shift / ⌘ 点击多选;目录可拖拽</span></div>
430
+ <div class="dshwe-tree" id="tree"></div>
431
+ <div class="dshwe-selbar" id="selbar" hidden>
432
+ <span class="dshwe-selbar-count" id="selCount">已选 0 项</span>
433
+ <button type="button" class="dshwe-prevbtn dshwe-prevbtn-primary" id="selInsert">插入所选</button>
434
+ <button type="button" class="dshwe-prevbtn" id="selClear">清除</button>
435
+ </div>
436
+ </div>
437
+ </div>
438
+ </div>
439
+
440
+ <!-- 设置页 -->
441
+ <div class="dshwe-tabbody" id="tabSettings" hidden>
442
+ <div class="dshwe-set">
443
+ <div class="dshwe-setsec" id="setGeneral">通用</div>
444
+ <div class="dshwe-setrow">
445
+ <div class="dshwe-setinfo"><div class="dshwe-setlabel" id="setNoise">隐藏噪声目录</div><div class="dshwe-setcap">.git · node_modules · dist 等</div></div>
446
+ <button type="button" role="switch" aria-checked="true" class="dshwe-switch" data-key="hideNoise"></button>
447
+ </div>
448
+ <div class="dshwe-setrow">
449
+ <div class="dshwe-setinfo"><div class="dshwe-setlabel" id="setSize">显示文件大小</div></div>
450
+ <button type="button" role="switch" aria-checked="true" class="dshwe-switch" data-key="showSize"></button>
451
+ </div>
452
+ <div class="dshwe-setrow">
453
+ <div class="dshwe-setinfo"><div class="dshwe-setlabel" id="setRef">文件引用格式</div></div>
454
+ <select class="dshwe-setselect" id="setRefSel">
455
+ <option value="relative">相对路径</option>
456
+ <option value="absolute">绝对路径</option>
457
+ </select>
458
+ </div>
459
+ <div class="dshwe-setrow">
460
+ <div class="dshwe-setinfo"><div class="dshwe-setlabel" id="setPeek">预览行数</div></div>
461
+ <select class="dshwe-setselect" id="setPeekSel">
462
+ <option value="30">30</option>
463
+ <option value="60" selected>60</option>
464
+ <option value="120">120</option>
465
+ </select>
466
+ </div>
467
+ <div class="dshwe-setrow">
468
+ <div class="dshwe-setinfo"><div class="dshwe-setlabel" id="setWidth">面板宽度</div></div>
469
+ <select class="dshwe-setselect" id="setWidthSel">
470
+ <option value="320">紧凑 · 320</option>
471
+ <option value="384" selected>标准 · 384</option>
472
+ <option value="480">宽松 · 480</option>
473
+ </select>
474
+ </div>
475
+ <div class="dshwe-setfoot"><button type="button" class="dshwe-prevbtn" id="setReset">恢复默认</button></div>
476
+ <div class="dshwe-setnote" id="setNote">配置在本次会话内生效,重启插件后恢复默认。</div>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ </div>
482
+
483
+ <script>
484
+ (function () {
485
+ 'use strict'
486
+
487
+ // ---------- 图标(SVG 路径与插件一致) ----------
488
+ var FOLDER_D = 'M1.5 2.5A1.5 1.5 0 0 1 3 1h3.2l1.6 2H13a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 13 13H3a1.5 1.5 0 0 1-1.5-1.5v-9z'
489
+ var DOC_BODY = 'M4.3 1.7h5.3l2.7 2.7v8.9a1 1 0 0 1-1 1H4.3a1 1 0 0 1-1-1V2.7a1 1 0 0 1 1-1z'
490
+ var DOC_FOLD = 'M9.6 1.7L12.3 4.4H9.6z'
491
+ var GLYPHS = {
492
+ code: 'M6.4 6.1L4.9 8l1.5 1.9M9.6 6.1l1.5 1.9L9.6 9.9',
493
+ image: 'M3.6 12.4l2.7-2.7 1.8 1.8 1.5-1.5 2.8 2.4M5.4 6.4a1.1 1.1 0 1 0 0-2.2 1.1 1.1 0 0 0 0 2.2z',
494
+ markdown: 'M5.1 11.2l1.9-2.9 1.9 2.9M5.1 8.4h3.8',
495
+ config: 'M3.2 5.6h3.6M9 5.6h3.8M3.2 10.4h3.6M9 10.4h3.8M7.4 3.8v3.6M7.4 8.6v3.6',
496
+ css: 'M6.2 4.4v7.2M9.8 4.4v7.2M4.9 6.9h6.2M4.9 9.1h6.2',
497
+ shell: 'M3.6 5.4l2.4 2.6L3.6 10.6M8.1 10.6h4.3',
498
+ plain: ''
499
+ }
500
+ var FILE_META = {
501
+ ts: ['#3178c6', 'code'], tsx: ['#3178c6', 'code'], py: ['#3572a5', 'code'], js: ['#d4a72c', 'code'],
502
+ sh: ['#4c9a4a', 'shell'], json: ['#c9a227', 'config'], yml: ['#5b7c99', 'config'], yaml: ['#5b7c99', 'config'],
503
+ md: ['#4f8ac9', 'markdown'], txt: ['#8a919c', 'plain'], css: ['#2965f1', 'css'], scss: ['#c6538c', 'css'],
504
+ svg: ['#5a67d8', 'image'], png: ['#5a67d8', 'image'], ico: ['#5a67d8', 'image'], csv: ['#2fa97c', 'plain']
505
+ }
506
+ var DEFAULT_META = ['#8a919c', 'plain']
507
+ var NOISE_DIRS = ['node_modules', '.git', 'dist', '__pycache__', '.venv', 'build', '.next']
508
+
509
+ function extOf(name) { var i = name.lastIndexOf('.'); return i <= 0 ? '' : name.slice(i + 1).toLowerCase() }
510
+ function svg(paths, size) {
511
+ var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
512
+ s.setAttribute('viewBox', '0 0 16 16'); s.setAttribute('width', size); s.setAttribute('height', size)
513
+ s.setAttribute('aria-hidden', 'true'); s.style.display = 'inline-flex'
514
+ paths.forEach(function (p) {
515
+ var el = document.createElementNS('http://www.w3.org/2000/svg', 'path')
516
+ Object.keys(p).forEach(function (k) { el.setAttribute(k, p[k]) })
517
+ s.appendChild(el)
518
+ })
519
+ return s
520
+ }
521
+ function folderSvg(open) { return svg([{ d: FOLDER_D, fill: open ? '#e8c47c' : '#dcb67a' }], 16) }
522
+ function fileSvg(color, glyph) {
523
+ var paths = [{ d: DOC_BODY, fill: color }, { d: DOC_FOLD, fill: 'rgba(255,255,255,.92)' }]
524
+ if (glyph) paths.push({ d: glyph, fill: 'none', stroke: '#fff', 'stroke-width': 1.5, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' })
525
+ return svg(paths, 16)
526
+ }
527
+ function chevronSvg(open) {
528
+ return svg([{ d: 'M6 4l4 4-4 4', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.5, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }], 14)
529
+ }
530
+
531
+ // ---------- 模拟工作区数据(文件带行数,供分页预览) ----------
532
+ var WORKSPACES = {
533
+ 'my-app': {
534
+ label: 'my-app',
535
+ dirs: {
536
+ 'src': {
537
+ dirs: {
538
+ 'components': { dirs: {}, files: [['Button.tsx', 1284, 140], ['Card.tsx', 2048, 96]] }
539
+ },
540
+ files: [['app.tsx', 5621, 180], ['api.ts', 3390, 120], ['styles.css', 1874, 88]]
541
+ },
542
+ 'public': { dirs: {}, files: [['logo.svg', 8934, 40], ['favicon.ico', 15486, 1]] }
543
+ },
544
+ files: [['package.json', 2411, 60], ['README.md', 1189, 45], ['tsconfig.json', 742, 30], ['vite.config.ts', 918, 50], ['.env', 120, 8]]
545
+ },
546
+ 'data-pipeline': {
547
+ label: 'data-pipeline',
548
+ dirs: {
549
+ 'src': { dirs: {}, files: [['main.py', 4922, 150], ['models.py', 3810, 210], ['utils.py', 2214, 95]] },
550
+ 'scripts': { dirs: {}, files: [['run.sh', 342, 20]] },
551
+ 'data': { dirs: {}, files: [['sample.csv', 12840, 300], ['schema.json', 861, 40]] }
552
+ },
553
+ files: [['requirements.txt', 210, 12], ['pyproject.toml', 1502, 55], ['README.md', 963, 40], ['.env', 88, 6]]
554
+ }
555
+ }
556
+
557
+ function fmtSize(n) {
558
+ if (n < 1024) return n + ' B'
559
+ if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'
560
+ return (n / 1048576).toFixed(1) + ' MB'
561
+ }
562
+
563
+ // ---------- 状态 ----------
564
+ var state = {
565
+ open: false,
566
+ tab: 'files',
567
+ ws: 'my-app',
568
+ expanded: {},
569
+ selected: new Set(),
570
+ selAnchor: null,
571
+ filter: '',
572
+ preview: null, // { rel, name, size, lines, page, mode: 'preview' | 'edit', editContent, dirty, saving, saveError }
573
+ cfg: { hideNoise: true, showSize: true, refStyle: 'relative', peekLines: 60, width: 384 }
574
+ }
575
+
576
+ var $ = function (id) { return document.getElementById(id) }
577
+ var popup = $('popup'), layer = $('layer'), tree = $('tree'), wsSel = $('wsSel')
578
+ var logEl = $('log'), composer = $('composer'), dropHint = $('dropHint'), composerCard = $('composerCard')
579
+
580
+ // 工作区下拉
581
+ Object.keys(WORKSPACES).forEach(function (k) {
582
+ var o = document.createElement('option')
583
+ o.value = k; o.textContent = WORKSPACES[k].label
584
+ wsSel.appendChild(o)
585
+ })
586
+ wsSel.value = state.ws
587
+
588
+ // ---------- 语言切换 ----------
589
+ var STR = {
590
+ zh: {
591
+ 'panel.title': '工作区文件', hint: '👁 预览/编辑;点击插入;Shift / ⌘ 多选;目录可拖拽',
592
+ dropHint: '松开以插入文件引用到输入框', dropHintDir: '松开以插入目录树',
593
+ composerPh: '在此输入… 或点击/拖拽右侧文件',
594
+ send: '发送', files: '文件', settings: '设置', logEmpty: '发送文件引用后,这里会出现 `[file: …]` 消息(模拟)。',
595
+ langBtn: 'English', rowTip: '点击或拖拽到输入框', insertTip: '插入引用', previewTip: '预览 / 编辑',
596
+ refresh: '刷新', close: '关闭', searchPh: '搜索文件…', clear: '清除',
597
+ selCount: '已选 {n} 项', selInsert: '插入所选', previewPage: '第 {n} 页', previewLines: '{n} 行',
598
+ prev: '上一页', next: '下一页', insertRef: '插入引用', insertContent: '插入内容',
599
+ edit: '编辑', editSave: '保存', editDiscard: '放弃', editCancel: '取消', editDirty: '已修改', editSaved: '已保存', editSaving: '保存中…', editDiscardConfirm: '放弃修改?',
600
+ hit: '匹配 {n} 项', hitNone: '没有匹配「{q}」的文件',
601
+ toggleLabel: '工作区文件', toggleTitle: '文件目录', wsCrumb: 'workspace', wsCur: 'my-app',
602
+ general: '通用', setNoise: '隐藏噪声目录', setNoiseDesc: '.git · node_modules · dist 等',
603
+ setSize: '显示文件大小', setRef: '文件引用格式', rel: '相对路径', abs: '绝对路径',
604
+ setPeek: '预览行数', setWidth: '面板宽度', widthNarrow: '紧凑 · 320', widthStd: '标准 · 384', widthWide: '宽松 · 480',
605
+ reset: '恢复默认', setNote: '配置在本次会话内生效,重启插件后恢复默认。'
606
+ },
607
+ en: {
608
+ 'panel.title': 'Workspace Files', hint: '👁 preview / edit; click to insert; Shift / ⌘ multi-select; drag folders',
609
+ dropHint: 'Release to insert the file reference into the composer', dropHintDir: 'Release to insert the folder tree',
610
+ composerPh: 'Type here… or click / drag files on the right',
611
+ send: 'Send', files: 'Files', settings: 'Settings', logEmpty: 'File references sent here appear as `[file: …]` messages (mock).',
612
+ langBtn: '中文', rowTip: 'click or drag to the composer', insertTip: 'Insert reference', previewTip: 'Preview / Edit',
613
+ refresh: 'Refresh', close: 'Close', searchPh: 'Search files…', clear: 'Clear',
614
+ selCount: '{n} selected', selInsert: 'Insert', previewPage: 'Page {n}', previewLines: '{n} lines',
615
+ prev: 'Previous page', next: 'Next page', insertRef: 'Insert reference', insertContent: 'Insert content',
616
+ edit: 'Edit', editSave: 'Save', editDiscard: 'Discard', editCancel: 'Cancel', editDirty: 'Modified', editSaved: 'Saved', editSaving: 'Saving…', editDiscardConfirm: 'Discard changes?',
617
+ hit: '{n} match(es)', hitNone: 'No files match "{q}"',
618
+ toggleLabel: 'Workspace Files', toggleTitle: 'Files', wsCrumb: 'workspace', wsCur: 'my-app',
619
+ general: 'General', setNoise: 'Hide noise dirs', setNoiseDesc: '.git · node_modules · dist …',
620
+ setSize: 'Show file sizes', setRef: 'Reference format', rel: 'Relative', abs: 'Absolute',
621
+ setPeek: 'Preview lines', setWidth: 'Panel width', widthNarrow: 'Narrow · 320', widthStd: 'Standard · 384', widthWide: 'Wide · 480',
622
+ reset: 'Reset to defaults', setNote: 'Settings apply for this run; they reset when the plugin restarts.'
623
+ }
624
+ }
625
+ var lang = 'zh'
626
+ var tr = function (k, vars) {
627
+ var s = STR[lang][k] || k
628
+ if (vars) for (var key in vars) s = s.split('{' + key + '}').join(String(vars[key]))
629
+ return s
630
+ }
631
+ function applyStrings() {
632
+ $('langBtn').textContent = tr('langBtn')
633
+ composer.placeholder = tr('composerPh')
634
+ $('sendBtn').textContent = tr('send')
635
+ $('toggleLabel').textContent = tr('toggleLabel')
636
+ $('toggleBtn').title = tr('toggleTitle')
637
+ $('tabFilesLabel').textContent = tr('files')
638
+ $('tabSettingsLabel').textContent = tr('settings')
639
+ $('filter').placeholder = tr('searchPh')
640
+ $('hintText').textContent = tr('hint')
641
+ $('crumbWs').textContent = tr('wsCrumb')
642
+ $('crumbCur').textContent = WORKSPACES[state.ws].label
643
+ $('setGeneral').textContent = tr('general')
644
+ $('setNoise').textContent = tr('setNoise')
645
+ document.querySelectorAll('#tabSettings .dshwe-setcap')[0].textContent = tr('setNoiseDesc')
646
+ $('setSize').textContent = tr('setSize')
647
+ $('setRef').textContent = tr('setRef')
648
+ $('setPeek').textContent = tr('setPeek')
649
+ $('setWidth').textContent = tr('setWidth')
650
+ $('setReset').textContent = tr('reset')
651
+ $('setNote').textContent = tr('setNote')
652
+ $('selInsert').textContent = tr('selInsert')
653
+ $('selClear').textContent = tr('clear')
654
+ $('previewRef').textContent = tr('insertRef')
655
+ $('previewContent').textContent = tr('insertContent')
656
+ $('previewEdit').textContent = tr('edit')
657
+ $('editSave').textContent = tr('editSave')
658
+ $('editDiscard').textContent = tr('editDiscard')
659
+ $('editCancel').title = tr('editCancel')
660
+ $('previewClose').title = tr('close')
661
+ var le = document.getElementById('logEmpty'); if (le) le.textContent = tr('logEmpty')
662
+ var dh = document.getElementById('dropHintText'); if (dh) dh.textContent = tr('dropHint')
663
+ var opts = $('setRefSel').options
664
+ opts[0].textContent = tr('rel'); opts[1].textContent = tr('abs')
665
+ var wopts = $('setWidthSel').options
666
+ wopts[0].textContent = tr('widthNarrow'); wopts[1].textContent = tr('widthStd'); wopts[2].textContent = tr('widthWide')
667
+ renderPanel()
668
+ renderTree()
669
+ }
670
+ $('langBtn').addEventListener('click', function () { lang = lang === 'zh' ? 'en' : 'zh'; applyStrings() })
671
+
672
+ // ---------- 面板开关与 Tab ----------
673
+ function renderPanel() {
674
+ popup.hidden = !state.open
675
+ popup.classList.toggle('dshwe-popup-on', state.open)
676
+ document.getElementById('stage').classList.toggle('panel-open', state.open)
677
+ popup.style.width = (state.preview ? state.cfg.width + 340 : state.cfg.width) + 'px'
678
+ $('toggleBtn').classList.toggle('dshwe-hicon-on', state.open)
679
+ $('tabFiles').hidden = state.tab !== 'files'
680
+ $('tabSettings').hidden = state.tab !== 'settings'
681
+ $('tabFilesBtn').classList.toggle('dshwe-tab-on', state.tab === 'files')
682
+ $('tabSettingsBtn').classList.toggle('dshwe-tab-on', state.tab === 'settings')
683
+ }
684
+ $('toggleBtn').addEventListener('click', function () { state.open = !state.open; renderPanel() })
685
+ $('tabFilesBtn').addEventListener('click', function () { state.tab = 'files'; renderPanel() })
686
+ $('tabSettingsBtn').addEventListener('click', function () { state.tab = 'settings'; renderPanel() })
687
+ $('addBtn').addEventListener('click', function () {
688
+ var keys = Object.keys(WORKSPACES)
689
+ var next = keys[(keys.indexOf(state.ws) + 1) % keys.length]
690
+ switchWs(next)
691
+ })
692
+ wsSel.addEventListener('change', function () { switchWs(wsSel.value) })
693
+ function switchWs(ws) {
694
+ state.ws = ws; state.expanded = {}; state.selected = new Set(); state.selAnchor = null
695
+ state.filter = ''; state.preview = null
696
+ $('filter').value = ''; $('filterClear').hidden = true
697
+ $('crumbCur').textContent = WORKSPACES[ws].label
698
+ renderPanel(); renderTree()
699
+ }
700
+
701
+ // ---------- 目录树数据访问 ----------
702
+ function nodeAt(node, rel) {
703
+ if (rel === '') return node
704
+ var segs = rel.split('/')
705
+ var cur = node
706
+ for (var i = 0; i < segs.length; i++) {
707
+ if (!cur || !cur.dirs) return null
708
+ cur = cur.dirs[segs[i]]
709
+ }
710
+ return cur
711
+ }
712
+ function dirHasNoise(node, name) { return state.cfg.hideNoise && NOISE_DIRS.indexOf(name) >= 0 }
713
+
714
+ // ---------- 平铺可见条目(搜索 / 展开树) ----------
715
+ function flatVisible() {
716
+ var ws = WORKSPACES[state.ws]
717
+ var q = state.filter.trim().toLowerCase()
718
+ var out = []
719
+ if (q !== '') {
720
+ // 搜索:遍历整棵工作区树,匹配文件名
721
+ var walkAll = function (node, baseRel) {
722
+ var dirNames = Object.keys(node.dirs || {}).sort()
723
+ dirNames.forEach(function (name) {
724
+ if (dirHasNoise(node, name)) return
725
+ var rel = baseRel === '' ? name : baseRel + '/' + name
726
+ out.push({ name: name, type: 'directory', rel: rel, dir: node.dirs[name], depth: 0 })
727
+ walkAll(node.dirs[name], rel)
728
+ })
729
+ ;(node.files || []).forEach(function (f) {
730
+ if (f[0].toLowerCase().indexOf(q) >= 0) {
731
+ var rel2 = baseRel === '' ? f[0] : baseRel + '/' + f[0]
732
+ out.push({ name: f[0], type: 'file', rel: rel2, size: f[1], lines: f[2], depth: 0 })
733
+ }
734
+ })
735
+ }
736
+ walkAll(ws, '')
737
+ } else {
738
+ var walkExp = function (node, baseRel, depth) {
739
+ var dirNames = Object.keys(node.dirs || {}).sort()
740
+ dirNames.forEach(function (name) {
741
+ if (dirHasNoise(node, name)) return
742
+ var rel = baseRel === '' ? name : baseRel + '/' + name
743
+ out.push({ name: name, type: 'directory', rel: rel, dir: node.dirs[name], depth: depth })
744
+ if (state.expanded[rel]) walkExp(node.dirs[name], rel, depth + 1)
745
+ })
746
+ ;(node.files || []).slice().sort(function (a, b) { return a[0].localeCompare(b[0]) })
747
+ .forEach(function (f) {
748
+ var rel2 = baseRel === '' ? f[0] : baseRel + '/' + f[0]
749
+ out.push({ name: f[0], type: 'file', rel: rel2, size: f[1], lines: f[2], depth: depth })
750
+ })
751
+ }
752
+ walkExp(ws, '', 0)
753
+ }
754
+ return out
755
+ }
756
+
757
+ // ---------- 渲染树 ----------
758
+ function renderTree() {
759
+ tree.textContent = ''
760
+ var list = flatVisible()
761
+ var q = state.filter.trim()
762
+ if (q !== '') {
763
+ var note = document.createElement('div')
764
+ note.className = 'dshwe-tree-note'
765
+ note.textContent = list.length ? tr('hit', { n: list.length }) : tr('hitNone', { q: q })
766
+ tree.appendChild(note)
767
+ }
768
+ list.forEach(function (entry) { tree.appendChild(rowFor(entry)) })
769
+ // 选中操作条
770
+ var n = state.selected.size
771
+ $('selbar').hidden = n === 0
772
+ $('selCount').textContent = tr('selCount', { n: n })
773
+ }
774
+
775
+ function rowFor(entry) {
776
+ var isDir = entry.type === 'directory'
777
+ var isSel = state.selected.has(entry.rel)
778
+ var isPrev = !!state.preview && state.preview.rel === entry.rel
779
+ var row = document.createElement('button')
780
+ row.type = 'button'
781
+ row.className = 'dshwe-row' + (isDir ? ' dshwe-row-dir' : ' dshwe-row-file') + (isSel ? ' dshwe-row-sel' : '') + (isPrev ? ' dshwe-row-active' : '')
782
+ row.style.paddingLeft = (10 + entry.depth * 16) + 'px'
783
+ row.title = entry.rel + (isDir ? '' : ' · ' + tr('rowTip'))
784
+ row.draggable = true
785
+
786
+ var slot = document.createElement('span'); slot.className = 'dshwe-chev-slot'
787
+ if (isDir) {
788
+ var open = !!state.expanded[entry.rel]
789
+ var ch = chevronSvg(open); ch.classList.toggle('dshwe-chev-on', open); slot.appendChild(ch)
790
+ }
791
+ row.appendChild(slot)
792
+ if (!isDir) {
793
+ // 👁 预览 / 编辑按钮(常显,与真实插件一致;点击不触发行插入)
794
+ var pbtn = document.createElement('span'); pbtn.className = 'dshwe-preview-btn' + (isPrev ? ' dshwe-preview-btn-on' : ''); pbtn.title = tr('previewTip')
795
+ pbtn.appendChild(svg([{ d: 'M1.5 8s2.6-4.5 6.5-4.5S14.5 8 14.5 8 11.9 12.5 8 12.5 1.5 8 1.5 8zM8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4z', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.3, 'stroke-linejoin': 'round' }], 15))
796
+ pbtn.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); openPreview(entry) })
797
+ pbtn.addEventListener('click', function (e) { e.stopPropagation() })
798
+ row.appendChild(pbtn)
799
+ }
800
+ var meta = FILE_META[extOf(entry.name)] || DEFAULT_META
801
+ row.appendChild(isDir ? folderSvg(!!state.expanded[entry.rel]) : fileSvg(meta[0], GLYPHS[meta[1]] || ''))
802
+ var nm = document.createElement('span'); nm.className = 'dshwe-name'; nm.textContent = entry.name; row.appendChild(nm)
803
+
804
+ if (!isDir && state.cfg.showSize) {
805
+ var sz = document.createElement('span'); sz.className = 'dshwe-size'; sz.textContent = fmtSize(entry.size); row.appendChild(sz)
806
+ }
807
+ if (!isDir && isPrev) {
808
+ var dot = document.createElement('span'); dot.className = 'dshwe-row-preview-dot'; row.appendChild(dot)
809
+ }
810
+
811
+ row.addEventListener('click', function (e) { onRowClick(e, entry) })
812
+ row.addEventListener('dragstart', function (e) { onDragStart(e, entry) })
813
+ return row
814
+ }
815
+
816
+ // ---------- 行交互:目录展开 / 文件插入 / Shift·⌘ 多选 ----------
817
+ function markerFor(entry) { return '[file: ' + entry.rel + ']' }
818
+ function insertMarker(entry) { appendToComposer(markerFor(entry)) }
819
+ function appendToComposer(text) {
820
+ var draft = composer.value
821
+ var sep = draft === '' || draft.endsWith('\n') ? '' : '\n'
822
+ composer.value = draft + sep + text
823
+ composer.focus()
824
+ composer.setSelectionRange(composer.value.length, composer.value.length)
825
+ }
826
+ function onRowClick(e, entry) {
827
+ // 忽略来自 👁 预览按钮的点击(其 mousedown 已打开预览,click 目标可能因布局位移落在行上)
828
+ if (e.target && e.target.closest && e.target.closest('.dshwe-preview-btn')) return
829
+ var isDir = entry.type === 'directory'
830
+ if (e.shiftKey || e.metaKey || e.ctrlKey) {
831
+ e.preventDefault()
832
+ if (e.shiftKey && state.selAnchor !== null) {
833
+ var list = flatVisible()
834
+ var a = -1, b = -1
835
+ list.forEach(function (x, i) { if (x.rel === state.selAnchor) a = i; if (x.rel === entry.rel) b = i })
836
+ if (a >= 0 && b >= 0) {
837
+ var lo = a < b ? a : b, hi = a < b ? b : a
838
+ for (var i = lo; i <= hi; i++) state.selected.add(list[i].rel)
839
+ }
840
+ } else if (state.selected.has(entry.rel)) {
841
+ state.selected.delete(entry.rel)
842
+ } else {
843
+ state.selected.add(entry.rel)
844
+ }
845
+ state.selAnchor = entry.rel
846
+ renderTree()
847
+ } else if (isDir) {
848
+ if (state.expanded[entry.rel]) delete state.expanded[entry.rel]
849
+ else state.expanded[entry.rel] = true
850
+ renderTree()
851
+ } else {
852
+ insertMarker(entry)
853
+ }
854
+ }
855
+
856
+ // ---------- 批量插入所选(文件 → 引用;目录 → 目录树) ----------
857
+ function insertSelected() {
858
+ var parts = []
859
+ flatVisible().forEach(function (e) {
860
+ if (!state.selected.has(e.rel)) return
861
+ if (e.type === 'directory') {
862
+ var text = formatTreeBlock(e)
863
+ if (text) parts.push(text)
864
+ } else {
865
+ parts.push(markerFor(e))
866
+ }
867
+ })
868
+ if (parts.length) appendToComposer(parts.join('\n'))
869
+ state.selected = new Set()
870
+ state.selAnchor = null
871
+ renderTree()
872
+ }
873
+ $('selInsert').addEventListener('click', insertSelected)
874
+ $('selClear').addEventListener('click', function () { state.selected = new Set(); state.selAnchor = null; renderTree() })
875
+
876
+ // ---------- 目录树文本(与插件一致:├── 树形、限 3 层、噪声过滤) ----------
877
+ function formatTreeBlock(entry) {
878
+ var node = entry.dir
879
+ var out = []
880
+ var budget = 200
881
+ var truncated = false
882
+ var walk = function (n, prefix, isLast, isRoot, depth) {
883
+ if (depth > 3) { truncated = true; return }
884
+ if (isRoot) {
885
+ out.push(entry.rel + '/')
886
+ } else {
887
+ out.push(prefix + (isLast ? '└── ' : '├── ') + n.name + (n.isDir ? '/' : ''))
888
+ prefix += isLast ? ' ' : '│ '
889
+ }
890
+ if (out.length > budget) { truncated = true; return }
891
+ var kids = []
892
+ Object.keys(n.dirs || {}).sort().forEach(function (name) {
893
+ if (!dirHasNoise(n, name)) kids.push({ name: name, isDir: true, dir: n.dirs[name] })
894
+ })
895
+ ;(n.files || []).slice().sort(function (a, b) { return a[0].localeCompare(b[0]) })
896
+ .forEach(function (f) { kids.push({ name: f[0], isDir: false }) })
897
+ kids.forEach(function (k, i) {
898
+ if (out.length > budget) { truncated = true; return }
899
+ walk(k, prefix, i === kids.length - 1, false, depth + 1)
900
+ })
901
+ }
902
+ walk(node, '', true, true, 1)
903
+ if (truncated) out.push('…')
904
+ return out.join('\n')
905
+ }
906
+
907
+ // ---------- 拖拽:文件 → 引用;目录 → 目录树文本 ----------
908
+ function onDragStart(e, entry) {
909
+ e.dataTransfer.setData('text/plain', entry.type === 'directory' ? entry.name : markerFor(entry))
910
+ e.dataTransfer.setData('application/x-dsh-ws-file', JSON.stringify({ rel: entry.rel, type: entry.type }))
911
+ e.dataTransfer.effectAllowed = 'copy'
912
+ $('dropHintText').textContent = entry.type === 'directory' ? tr('dropHintDir') : tr('dropHint')
913
+ dropHint.hidden = false
914
+ }
915
+ document.addEventListener('dragover', function (e) {
916
+ if (!(e.dataTransfer && Array.prototype.indexOf.call(e.dataTransfer.types || [], 'application/x-dsh-ws-file') >= 0)) return
917
+ e.preventDefault()
918
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
919
+ }, true)
920
+ document.addEventListener('drop', function (e) {
921
+ if (!(e.dataTransfer && Array.prototype.indexOf.call(e.dataTransfer.types || [], 'application/x-dsh-ws-file') >= 0)) return
922
+ var raw = e.dataTransfer.getData('application/x-dsh-ws-file') || ''
923
+ var payload = null
924
+ try { payload = raw ? JSON.parse(raw) : null } catch (err) { payload = null }
925
+ var ta = composerCard.contains(e.target) ? composer : null
926
+ dropHint.hidden = true
927
+ if (payload && payload.type === 'directory') {
928
+ // 目录:任意落点都插入目录树文本
929
+ e.preventDefault(); e.stopPropagation()
930
+ var entry = null
931
+ flatVisible().forEach(function (x) { if (x.rel === payload.rel) entry = x })
932
+ if (!entry) entry = { rel: payload.rel, name: payload.rel.split('/').pop(), type: 'directory', dir: nodeAt(WORKSPACES[state.ws], payload.rel) }
933
+ if (entry && entry.dir) appendToComposer(formatTreeBlock(entry))
934
+ return
935
+ }
936
+ if (ta != null) { return } // 输入框内:原生光标插入(演示直接追加)
937
+ e.preventDefault()
938
+ e.stopPropagation()
939
+ var text = e.dataTransfer.getData('text/plain') || ''
940
+ if (text) { composer.value = composer.value ? composer.value + '\n' + text : text; composer.focus() }
941
+ }, true)
942
+ document.addEventListener('dragend', function () { dropHint.hidden = true })
943
+
944
+ // ---------- 分屏预览 / 编辑(左侧 340px 面板,右侧文件树保持可见;与真实插件一致) ----------
945
+ function fileEntry(rel) {
946
+ var found = null
947
+ flatVisible().forEach(function (x) { if (x.rel === rel) found = x })
948
+ return found
949
+ }
950
+ function openPreview(entry) {
951
+ state.preview = { rel: entry.rel, name: entry.name, size: entry.size, lines: entry.lines || 60, page: 0, mode: 'preview', editContent: '', dirty: false, saving: false, saveError: null }
952
+ renderPreview()
953
+ }
954
+ function enterEdit() {
955
+ var p = state.preview
956
+ if (!p) return
957
+ var total = p.lines || 60
958
+ var cap = Math.min(total, 120)
959
+ var lines = []
960
+ for (var i = 0; i < cap; i++) lines.push(previewLine(p.name, p.rel, i))
961
+ p.editContent = lines.join('\n')
962
+ p.mode = 'edit'; p.dirty = false; p.saving = false; p.saveError = null
963
+ renderPreview()
964
+ }
965
+ function updateEditContent() {
966
+ var p = state.preview
967
+ if (!p) return
968
+ p.editContent = $('previewEditor').value
969
+ p.dirty = true
970
+ renderPreview()
971
+ }
972
+ function saveFile() {
973
+ var p = state.preview
974
+ if (!p || p.mode !== 'edit' || !p.dirty || p.saving) return
975
+ p.saving = true; p.saveError = null
976
+ renderPreview()
977
+ // 模拟写盘:短暂展示保存中,成功后回到预览模式并更新行数
978
+ setTimeout(function () {
979
+ if (!state.preview || state.preview !== p) return
980
+ p.saving = false; p.dirty = false
981
+ p.lines = Math.max(1, p.editContent.split('\n').length)
982
+ p.page = 0
983
+ p.mode = 'preview'
984
+ var e = fileEntry(p.rel)
985
+ if (e) e.lines = p.lines
986
+ renderPreview()
987
+ }, 650)
988
+ }
989
+ function discardEdit() {
990
+ var p = state.preview
991
+ if (!p) return
992
+ if (p.dirty && !window.confirm(tr('editDiscardConfirm', {}))) return
993
+ p.mode = 'preview'; p.dirty = false; p.saving = false; p.saveError = null
994
+ renderPreview()
995
+ }
996
+ function cancelEdit() {
997
+ var p = state.preview
998
+ if (!p) return
999
+ p.mode = 'preview'; p.dirty = false; p.saving = false; p.saveError = null
1000
+ renderPreview()
1001
+ }
1002
+ function closePreview() { state.preview = null; renderPreview() }
1003
+ function renderPreview() {
1004
+ var p = state.preview
1005
+ var slot = $('previewSlot')
1006
+ slot.hidden = !p
1007
+ document.getElementById('stage').classList.toggle('preview-open', !!p)
1008
+ popup.style.width = p ? (state.cfg.width + 340) + 'px' : state.cfg.width + 'px'
1009
+ if (!p) return
1010
+ $('previewName').textContent = p.name
1011
+ var isEdit = p.mode === 'edit'
1012
+ var per = state.cfg.peekLines
1013
+ var pages = Math.max(1, Math.ceil(p.lines / per))
1014
+ var metaBits = []
1015
+ if (p.size != null) metaBits.push(fmtSize(p.size))
1016
+ metaBits.push(tr('previewLines', { n: p.lines }))
1017
+ if (!isEdit && p.page > 0) metaBits.push(tr('previewPage', { n: p.page + 1 }))
1018
+ if (isEdit && p.dirty) metaBits.push(tr('editDirty'))
1019
+ $('previewMeta').textContent = metaBits.join(' · ')
1020
+ $('previewSaving').textContent = tr('editSaving', {})
1021
+ $('previewSaving').hidden = !(isEdit && p.saving)
1022
+ $('previewSaveErr').textContent = p.saveError || ''
1023
+ $('previewSaveErr').hidden = !p.saveError
1024
+ $('previewModeBtns').hidden = isEdit
1025
+ $('editModeBtns').hidden = !isEdit
1026
+ $('editSave').disabled = p.saving || !p.dirty
1027
+ $('previewActions').hidden = isEdit
1028
+ if (isEdit) {
1029
+ $('previewPre').hidden = true
1030
+ $('previewEditor').hidden = false
1031
+ if (document.activeElement !== $('previewEditor')) $('previewEditor').value = p.editContent
1032
+ } else {
1033
+ $('previewEditor').hidden = true
1034
+ $('previewPre').hidden = false
1035
+ var start = p.page * per
1036
+ var lines = []
1037
+ for (var i = start; i < Math.min(start + per, p.lines); i++) {
1038
+ lines.push(String(i + 1).padStart(4, ' ') + ' ' + previewLine(p.name, p.rel, i))
1039
+ }
1040
+ $('previewPre').textContent = lines.join('\n')
1041
+ $('prevPage').disabled = p.page === 0
1042
+ $('nextPage').disabled = p.page >= pages - 1
1043
+ }
1044
+ renderTree() // 重渲染树:激活行高亮 + 蓝点跟随当前预览文件
1045
+ }
1046
+ function previewLine(name, rel, i) {
1047
+ var ext = extOf(name)
1048
+ if (ext === 'json') return i === 0 ? '{' : ' "' + ['name', 'version', 'main', 'scripts', 'deps'][i % 5] + '": ' + (i % 2 ? 'true,' : '"value",')
1049
+ if (ext === 'css') return i === 0 ? '.' + name.replace(/\.[^.]+$/, '') + ' {' : ' ' + ['color', 'margin', 'padding', 'border-radius', 'display'][i % 5] + ': ' + ['#333', '0', '8px', '12px', 'flex'][i % 5] + ';'
1050
+ if (ext === 'md') return i === 0 ? '# ' + name : ['## Section', 'Some **markdown** content here.', '```ts', 'const x = 1', '```'][i % 5]
1051
+ if (ext === 'py') return i === 0 ? 'def ' + name.replace(/\.py$/, '') + '():' : ' ' + ['pass', 'return True', 'print("hi")', 'value = data[i]', 'yield result'][i % 5]
1052
+ if (ext === 'ts' || ext === 'tsx') return i === 0 ? 'export function ' + name.replace(/\.[^.]+$/, '') + '() {' : ' ' + ['const', 'return', 'await', 'if (x) {', '}'][i % 5] + ' ' + ['x = 1', 'result', 'fetchData()', 'console.log(x)', ''][i % 5]
1053
+ return 'line ' + (i + 1) + ' · ' + rel
1054
+ }
1055
+ $('prevPage').addEventListener('click', function () { if (state.preview && state.preview.page > 0) { state.preview.page--; renderPreview() } })
1056
+ $('nextPage').addEventListener('click', function () {
1057
+ if (state.preview) {
1058
+ var pages = Math.max(1, Math.ceil(state.preview.lines / state.cfg.peekLines))
1059
+ if (state.preview.page < pages - 1) { state.preview.page++; renderPreview() }
1060
+ }
1061
+ })
1062
+ $('previewRef').addEventListener('click', function () { if (state.preview) insertMarker({ rel: state.preview.rel }) })
1063
+ $('previewContent').addEventListener('click', function () { if (state.preview) appendToComposer('(内容已插入:' + state.preview.name + ' · 演示)') })
1064
+ $('previewEdit').addEventListener('click', enterEdit)
1065
+ $('editSave').addEventListener('click', saveFile)
1066
+ $('editDiscard').addEventListener('click', discardEdit)
1067
+ $('editCancel').addEventListener('click', cancelEdit)
1068
+ $('previewClose').addEventListener('click', closePreview)
1069
+ $('previewEditor').addEventListener('input', updateEditContent)
1070
+
1071
+ // ---------- 搜索过滤 ----------
1072
+ $('filter').addEventListener('input', function () {
1073
+ state.filter = this.value
1074
+ $('filterClear').hidden = state.filter === ''
1075
+ state.selected = new Set(); state.selAnchor = null
1076
+ renderTree()
1077
+ })
1078
+ $('filterClear').addEventListener('click', function () {
1079
+ state.filter = ''; $('filter').value = ''; $('filterClear').hidden = true
1080
+ renderTree()
1081
+ })
1082
+
1083
+ // ---------- 设置页(实时生效) ----------
1084
+ Array.prototype.forEach.call(document.querySelectorAll('.dshwe-switch'), function (btn) {
1085
+ btn.addEventListener('click', function () {
1086
+ var key = btn.getAttribute('data-key')
1087
+ var next = btn.getAttribute('aria-checked') !== 'true'
1088
+ btn.setAttribute('aria-checked', next)
1089
+ if (key === 'hideNoise') { state.cfg.hideNoise = next; state.expanded = {} }
1090
+ if (key === 'showSize') state.cfg.showSize = next
1091
+ renderTree()
1092
+ })
1093
+ })
1094
+ $('setRefSel').addEventListener('change', function () { state.cfg.refStyle = this.value })
1095
+ $('setPeekSel').addEventListener('change', function () {
1096
+ state.cfg.peekLines = Number(this.value)
1097
+ if (state.preview) renderPreview()
1098
+ })
1099
+ $('setWidthSel').addEventListener('change', function () {
1100
+ state.cfg.width = Number(this.value)
1101
+ popup.style.width = state.cfg.width + 'px'
1102
+ })
1103
+ $('setReset').addEventListener('click', function () {
1104
+ state.cfg = { hideNoise: true, showSize: true, refStyle: 'relative', peekLines: 60, width: 384 }
1105
+ $('setRefSel').value = 'relative'; $('setPeekSel').value = '60'; $('setWidthSel').value = '384'
1106
+ Array.prototype.forEach.call(document.querySelectorAll('.dshwe-switch'), function (b) { b.setAttribute('aria-checked', 'true') })
1107
+ popup.style.width = ''
1108
+ state.expanded = {}; state.selected = new Set(); state.preview = null
1109
+ renderTree()
1110
+ })
1111
+
1112
+ // ---------- 发送(模拟) ----------
1113
+ $('sendBtn').addEventListener('click', function () {
1114
+ var v = composer.value.trim()
1115
+ if (!v) return
1116
+ var m = document.createElement('div'); m.className = 'demo-msg'
1117
+ var esc = v.replace(/&/g, '&amp;').replace(/</g, '&lt;')
1118
+ m.innerHTML = esc.replace(/\[file:/g, '<span class="tag">[file:</span>')
1119
+ var empty = logEl.querySelector('.demo-log-empty'); if (empty) empty.remove()
1120
+ logEl.appendChild(m); logEl.scrollTop = logEl.scrollHeight
1121
+ composer.value = ''
1122
+ })
1123
+
1124
+ // ---------- 初始化 ----------
1125
+ applyStrings()
1126
+ })()
1127
+ </script>
1128
+ </body>
1129
+ </html>