@civilization/dsh-workspace-files 0.3.1 → 0.3.3

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.
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Browser entry for the enhanced workspace files explorer plugin.
3
+ * High-performance virtualized flat-row rendering with DSH Design Tokens and UI Motion.
3
4
  * Overrides DSH official 'files' tab using 'priority: extension'.
4
5
  */
5
6
  import type { Context } from '@deepseek-ai/cordis'
6
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
7
+ import React, { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
8
+ import type { JSX } from 'react'
7
9
  import {
8
10
  FileGlyphIcon,
9
11
  IconAt,
10
12
  IconCheck,
11
- IconChevronDown,
13
+ IconChevronRight,
12
14
  IconClose,
13
15
  IconCollapseAll,
14
16
  IconCopy,
@@ -24,6 +26,9 @@ import {
24
26
 
25
27
  export const inject = ['slots', 'sidebarRightTabs']
26
28
 
29
+ /** 构建时由 tsdown define 注入,避免卡片上的版本号与 package.json 漂移。 */
30
+ declare const __PLUGIN_VERSION__: string
31
+
27
32
  let globalCtx: Context | null = null
28
33
 
29
34
  const OFFICIAL_FILES_TAB_ID = '@civilization/dsh-workspace-files'
@@ -90,7 +95,6 @@ export function apply(ctx: Context): void {
90
95
  const slots = (ctx as any).slots
91
96
 
92
97
  if (sidebarRightTabs && slots) {
93
- // 关键:kind: 'files', priority: 'extension'(Rank 3 > builtin 2),完美接管官方文件标签页
94
98
  ctx.effect(() => sidebarRightTabs.register({
95
99
  id: OFFICIAL_FILES_TAB_ID,
96
100
  kind: 'files',
@@ -115,28 +119,16 @@ export function apply(ctx: Context): void {
115
119
  }, WorkspaceFilesTabTitle)), 'dsh-workspace-files: official tab title')
116
120
  }
117
121
 
118
- // 注入设置中心“插件配置”卡片(命名空间必须为 workspace-files,与 Host settings 服务注册一致)
119
- const registerSettingsCard = (targetCtx: any) => {
120
- if (!targetCtx?.slots) return
121
- targetCtx.effect?.(() => targetCtx.slots.inject('settings.plugin.item', function* () {
122
- yield targetCtx.slots.register({
123
- name: 'settings.plugin.item',
124
- key: 'workspace-files',
125
- }, WorkspaceFilesSettingsCard)
126
- yield targetCtx.slots.register({
127
- name: 'settings.plugin.item',
128
- key: '@civilization/dsh-workspace-files',
129
- }, WorkspaceFilesSettingsCard)
130
- }), 'dsh-workspace-files: settings card')
131
- }
132
-
122
+ // 设置中心“插件”页的自定义卡片:DSH 0.2.x 用 settings.plugins.tab(list 槽)承载
123
+ // 各插件自己的配置视图,注册需要 id/order/label;旧版的 settings.plugin.item 槽与
124
+ // settingsScope 服务在 0.2.0 已不存在。
133
125
  if (slots) {
134
- registerSettingsCard(ctx)
135
- }
136
- if ((ctx as any).inject) {
137
- (ctx as any).inject(['settingsScope'], (scoped: any) => {
138
- registerSettingsCard(scoped)
139
- })
126
+ ctx.effect(() => slots.inject('settings.plugins.tab', () => slots.register({
127
+ name: 'settings.plugins.tab',
128
+ id: 'workspace-files',
129
+ order: 20,
130
+ label: () => '工作区文件',
131
+ }, WorkspaceFilesSettingsCard)), 'dsh-workspace-files: settings card')
140
132
  }
141
133
  }
142
134
 
@@ -186,16 +178,16 @@ function ensureSettingsStyle() {
186
178
  style.id = id
187
179
  style.textContent = `
188
180
  .dsh-wf-card {
189
- border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
190
- background: var(--dsw-alias-bg-layer-3, #fff);
191
- border-radius: 12px;
181
+ border: 0.5px solid var(--dsw-alias-border-l1);
182
+ background: var(--dsw-alias-fill-base, #ffffff);
183
+ border-radius: var(--dsw-radius-md, 8px);
192
184
  list-style: none;
193
- transition: border-color .16s, background .16s;
185
+ transition: border-color 0.16s, background 0.16s;
194
186
  box-sizing: border-box;
195
187
  }
196
188
  .dsh-wf-card-open {
197
- background: var(--dsw-alias-bg-layer-2, #f7f8fa);
198
- border-color: var(--dsw-alias-label-dimmed, #c8ccd4);
189
+ background: var(--dsw-alias-fill-base, #ffffff);
190
+ border-color: var(--dsw-alias-state-business-primary);
199
191
  }
200
192
  .dsh-wf-header {
201
193
  -webkit-appearance: none;
@@ -205,24 +197,24 @@ function ensureSettingsStyle() {
205
197
  color: inherit;
206
198
  text-align: left;
207
199
  cursor: pointer;
208
- background: 0 0;
200
+ background: transparent;
209
201
  border: 0;
210
- border-radius: 12px;
202
+ border-radius: var(--dsw-radius-md, 8px);
211
203
  align-items: center;
212
204
  gap: 12px;
213
- padding: 14px 16px;
205
+ padding: 12px 14px;
214
206
  display: flex;
215
207
  }
216
208
  .dsh-wf-head-text {
217
209
  flex-direction: column;
218
210
  flex: 1;
219
- gap: 4px;
211
+ gap: 3px;
220
212
  min-width: 0;
221
213
  display: flex;
222
214
  }
223
215
  .dsh-wf-name {
224
- color: var(--dsw-alias-label-primary, #1f2328);
225
- font-size: 15px;
216
+ color: var(--dsw-alias-label-primary);
217
+ font-size: 14px;
226
218
  font-weight: 600;
227
219
  line-height: 1.4;
228
220
  display: flex;
@@ -230,38 +222,38 @@ function ensureSettingsStyle() {
230
222
  gap: 8px;
231
223
  }
232
224
  .dsh-wf-version {
233
- font-size: 12px;
225
+ font-size: 11px;
234
226
  font-weight: 400;
235
- color: var(--dsw-alias-label-tertiary, #8b93a1);
227
+ color: var(--dsw-alias-label-tertiary);
236
228
  }
237
229
  .dsh-wf-desc {
238
- color: var(--dsw-alias-label-tertiary, #8b93a1);
239
- font-size: 13px;
230
+ color: var(--dsw-alias-label-secondary);
231
+ font-size: 12px;
240
232
  line-height: 1.5;
241
233
  }
242
234
  .dsh-wf-chevron {
243
- color: var(--dsw-alias-label-tertiary, #8b93a1);
235
+ color: var(--dsw-alias-label-tertiary);
244
236
  flex: none;
245
- transition: transform .16s ease-in-out;
237
+ transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
246
238
  display: inline-flex;
247
239
  }
248
240
  .dsh-wf-chevron-open {
249
241
  transform: rotate(180deg);
250
242
  }
251
243
  .dsh-wf-body {
252
- border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
253
- margin: 0 16px;
244
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
245
+ margin: 0 14px;
254
246
  padding: 8px 0 12px;
255
247
  }
256
248
  .dsh-wf-row {
257
249
  align-items: center;
258
250
  gap: 16px;
259
- padding: 12px 0;
251
+ padding: 10px 0;
260
252
  display: flex;
261
253
  justify-content: space-between;
262
254
  }
263
255
  .dsh-wf-row + .dsh-wf-row {
264
- border-top: 1px solid var(--dsw-alias-border-l2, rgba(118, 137, 166, 0.12));
256
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
265
257
  }
266
258
  .dsh-wf-label-box {
267
259
  flex-direction: column;
@@ -271,14 +263,14 @@ function ensureSettingsStyle() {
271
263
  display: flex;
272
264
  }
273
265
  .dsh-wf-label {
274
- font-size: 13.5px;
266
+ font-size: 13px;
275
267
  font-weight: 550;
276
- color: var(--dsw-alias-label-primary, #1f2328);
268
+ color: var(--dsw-alias-label-primary);
277
269
  }
278
270
  .dsh-wf-hint {
279
- color: var(--dsw-alias-label-tertiary, #8b93a1);
280
- font-size: 12px;
281
- line-height: 18px;
271
+ color: var(--dsw-alias-label-tertiary);
272
+ font-size: 11.5px;
273
+ line-height: 16px;
282
274
  }
283
275
  .dsh-wf-ctrl {
284
276
  flex-shrink: 0;
@@ -287,56 +279,56 @@ function ensureSettingsStyle() {
287
279
  gap: 8px;
288
280
  }
289
281
  .dsh-wf-seg {
290
- border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
291
- border-radius: 8px;
282
+ border: 0.5px solid var(--dsw-alias-border-l1);
283
+ border-radius: var(--dsw-radius-sm, 6px);
292
284
  gap: 2px;
293
285
  padding: 2px;
294
286
  display: inline-flex;
295
- background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.05));
287
+ background: var(--dsw-alias-fill-base, #ffffff);
296
288
  }
297
289
  .dsh-wf-seg-btn {
298
290
  font: inherit;
299
- color: var(--dsw-alias-label-secondary, #6b7280);
291
+ color: var(--dsw-alias-label-secondary);
300
292
  cursor: pointer;
301
- background: 0 0;
293
+ background: transparent;
302
294
  border: none;
303
- border-radius: 6px;
304
- padding: 4px 10px;
305
- font-size: 12px;
306
- line-height: 18px;
307
- transition: all 0.15s;
295
+ border-radius: 4px;
296
+ padding: 3px 8px;
297
+ font-size: 11.5px;
298
+ transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1);
308
299
  }
309
300
  .dsh-wf-seg-btn:hover {
310
- color: var(--dsw-alias-label-primary, #1f2328);
301
+ color: var(--dsw-alias-label-primary);
302
+ background: var(--dsw-alias-interactive-bg-hover);
311
303
  }
312
304
  .dsh-wf-seg-active {
313
- background: var(--dsw-alias-bg-layer-3, #eef0f4);
314
- color: var(--dsw-alias-brand-primary, #4f6ef7);
305
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent);
306
+ color: var(--dsw-alias-state-business-primary);
315
307
  font-weight: 600;
316
- box-shadow: 0 1px 2px rgba(0,0,0,0.05);
317
308
  }
318
309
  .dsh-wf-input {
319
310
  box-sizing: border-box;
320
- border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
321
- background: var(--dsw-alias-bg-layer-3, #fff);
322
- width: 64px;
311
+ border: 0.5px solid var(--dsw-alias-border-l1);
312
+ background: var(--dsw-alias-fill-base, #ffffff);
313
+ width: 58px;
323
314
  text-align: center;
324
- color: var(--dsw-alias-label-primary, #1f2328);
315
+ color: var(--dsw-alias-label-primary);
325
316
  font: inherit;
326
- border-radius: 7px;
327
- padding: 4px 6px;
328
- font-size: 12.5px;
329
- line-height: 18px;
317
+ border-radius: 6px;
318
+ padding: 3px 6px;
319
+ font-size: 12px;
320
+ outline: none;
321
+ transition: border-color 0.15s, box-shadow 0.15s;
330
322
  }
331
323
  .dsh-wf-input:focus {
332
- border-color: var(--dsw-alias-brand-primary, #4f6ef7);
333
- outline: 2px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #4f6ef7) 18%, transparent);
324
+ border-color: var(--dsw-alias-state-business-primary);
325
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
334
326
  }
335
327
  .dsh-wf-toggle {
336
328
  position: relative;
337
329
  display: inline-block;
338
- width: 40px;
339
- height: 22px;
330
+ width: 36px;
331
+ height: 20px;
340
332
  cursor: pointer;
341
333
  }
342
334
  .dsh-wf-toggle input {
@@ -348,55 +340,55 @@ function ensureSettingsStyle() {
348
340
  .dsh-wf-toggle-track {
349
341
  position: absolute;
350
342
  top: 0; left: 0; right: 0; bottom: 0;
351
- background-color: var(--dsw-alias-border-l2, #ccc);
352
- transition: .2s cubic-bezier(.4, 0, .2, 1);
353
- border-radius: 22px;
343
+ background-color: var(--dsw-alias-border-l1);
344
+ transition: 0.18s cubic-bezier(0.16, 1, 0.3, 1);
345
+ border-radius: 20px;
354
346
  }
355
347
  .dsh-wf-toggle input:checked + .dsh-wf-toggle-track {
356
- background-color: var(--dsw-alias-brand-primary, #4f6ef7);
348
+ background-color: var(--dsw-alias-state-business-primary);
357
349
  }
358
350
  .dsh-wf-toggle-thumb {
359
351
  position: absolute;
360
- height: 16px;
361
- width: 16px;
352
+ height: 14px;
353
+ width: 14px;
362
354
  left: 3px;
363
355
  bottom: 3px;
364
- background-color: white;
365
- transition: .2s cubic-bezier(.4, 0, .2, 1);
356
+ background-color: #ffffff;
357
+ transition: 0.18s cubic-bezier(0.16, 1, 0.3, 1);
366
358
  border-radius: 50%;
367
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
359
+ box-shadow: 0 1px 2px rgba(0,0,0,0.18);
368
360
  }
369
361
  .dsh-wf-toggle input:checked + .dsh-wf-toggle-track .dsh-wf-toggle-thumb {
370
- transform: translateX(18px);
362
+ transform: translateX(16px);
371
363
  }
372
364
  .dsh-wf-footer {
373
- margin-top: 8px;
374
- padding-top: 10px;
375
- border-top: 1px solid var(--dsw-alias-border-l2, rgba(118, 137, 166, 0.12));
365
+ margin-top: 6px;
366
+ padding-top: 8px;
367
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
376
368
  display: flex;
377
369
  align-items: center;
378
370
  justify-content: space-between;
379
371
  }
380
372
  .dsh-wf-saved-hint {
381
- font-size: 11.5px;
382
- color: var(--dsw-alias-state-success-primary, #10b981);
373
+ font-size: 11px;
374
+ color: var(--dsw-alias-state-business-primary);
383
375
  display: flex;
384
376
  align-items: center;
385
377
  gap: 4px;
386
378
  }
387
379
  .dsh-wf-btn-reset {
388
380
  background: transparent;
389
- border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
390
- border-radius: 6px;
391
- padding: 3px 10px;
392
- font-size: 12px;
393
- color: var(--dsw-alias-label-secondary, #6b7280);
381
+ border: 0.5px solid var(--dsw-alias-border-l1);
382
+ border-radius: 4px;
383
+ padding: 3px 8px;
384
+ font-size: 11px;
385
+ color: var(--dsw-alias-label-secondary);
394
386
  cursor: pointer;
395
- transition: all 0.15s;
387
+ transition: all 0.12s;
396
388
  }
397
389
  .dsh-wf-btn-reset:hover {
398
- background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.08));
399
- color: var(--dsw-alias-label-primary, #1f2328);
390
+ background: var(--dsw-alias-interactive-bg-hover);
391
+ color: var(--dsw-alias-label-primary);
400
392
  }
401
393
  `
402
394
  document.head.appendChild(style)
@@ -438,29 +430,27 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
438
430
  const def = { maxDepth: 16, showHidden: false }
439
431
  setConfig(def)
440
432
  saveWorkspaceFilesConfig(def)
441
- setSavedTick(true)
442
- setTimeout(() => setSavedTick(false), 2000)
443
433
  }
444
434
 
445
435
  return (
446
- <div className={open ? 'dsh-wf-card dsh-wf-card-open' : 'dsh-wf-card'}>
436
+ <div className={`dsh-wf-card ${open ? 'dsh-wf-card-open' : ''}`}>
447
437
  <button
448
438
  type="button"
449
439
  className="dsh-wf-header"
450
- aria-expanded={open}
451
440
  onClick={() => setOpen(!open)}
452
441
  >
442
+ <FolderGuideGlyph size={22} />
453
443
  <div className="dsh-wf-head-text">
454
444
  <div className="dsh-wf-name">
455
445
  <span>工作区文件</span>
456
- <span className="dsh-wf-version">v0.3.0</span>
446
+ <span className="dsh-wf-version">v{__PLUGIN_VERSION__}</span>
457
447
  </div>
458
448
  <div className="dsh-wf-desc">
459
- 浏览与检索工作区文件树,支持递归目录扫描与一键引用。
449
+ 全量项目文件树浏览、即时检索与 @-mention 会话一键引用。
460
450
  </div>
461
451
  </div>
462
- <span className={open ? 'dsh-wf-chevron dsh-wf-chevron-open' : 'dsh-wf-chevron'}>
463
- <IconChevronDown size={14} />
452
+ <span className={`dsh-wf-chevron ${open ? 'dsh-wf-chevron-open' : ''}`}>
453
+ <IconChevronRight size={14} style={{ transform: open ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.16s' }} />
464
454
  </span>
465
455
  </button>
466
456
 
@@ -468,42 +458,42 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
468
458
  <div className="dsh-wf-body">
469
459
  <div className="dsh-wf-row">
470
460
  <div className="dsh-wf-label-box">
471
- <div className="dsh-wf-label">目录扫描最大深度</div>
472
- <div className="dsh-wf-hint">
473
- 工作区递归扫描的最大目录层级(默认 16,范围 1~100)。若工程目录较深可适当调大。
474
- </div>
461
+ <span className="dsh-wf-label">扫描最大深度 (层级)</span>
462
+ <span className="dsh-wf-hint">
463
+ 控制文件树深入遍历的最大文件夹嵌套层级,防止庞大项目深度过大产生卡顿。
464
+ </span>
475
465
  </div>
476
466
  <div className="dsh-wf-ctrl">
477
467
  <div className="dsh-wf-seg">
478
- {[8, 16, 32, 64].map((d) => (
468
+ {[8, 16, 24, 32].map(d => (
479
469
  <button
480
470
  key={d}
481
471
  type="button"
482
- className={config.maxDepth === d ? 'dsh-wf-seg-btn dsh-wf-seg-active' : 'dsh-wf-seg-btn'}
472
+ className={`dsh-wf-seg-btn ${config.maxDepth === d ? 'dsh-wf-seg-active' : ''}`}
483
473
  onClick={() => handleDepthSelect(d)}
484
474
  >
485
- {d}
475
+ {d} 层
486
476
  </button>
487
477
  ))}
488
478
  </div>
489
479
  <input
490
480
  type="number"
491
- min="1"
492
- max="100"
481
+ min={1}
482
+ max={100}
493
483
  className="dsh-wf-input"
494
484
  value={config.maxDepth}
495
485
  onChange={handleDepthInput}
496
- title="自定义深度 (1-100)"
486
+ title="手动输入层级深度 (1-100)"
497
487
  />
498
488
  </div>
499
489
  </div>
500
490
 
501
491
  <div className="dsh-wf-row">
502
492
  <div className="dsh-wf-label-box">
503
- <div className="dsh-wf-label">默认显示隐藏文件</div>
504
- <div className="dsh-wf-hint">
505
- 打开文件树时是否默认展示以点开头的隐藏文件或文件夹(如 .gitignore, .env 等)。
506
- </div>
493
+ <span className="dsh-wf-label">显示以点开头的隐藏文件</span>
494
+ <span className="dsh-wf-hint">
495
+ 开启后,文件树中将展现如 .env, .editorconfig 等以点开头的配置文件(.git 等缓存黑名单除外)。
496
+ </span>
507
497
  </div>
508
498
  <div className="dsh-wf-ctrl">
509
499
  <label className="dsh-wf-toggle">
@@ -521,14 +511,10 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
521
511
 
522
512
  <div className="dsh-wf-footer">
523
513
  <div>
524
- {savedTick ? (
514
+ {savedTick && (
525
515
  <span className="dsh-wf-saved-hint">
526
516
  <IconCheck size={12} />
527
- <span>已保存设置</span>
528
- </span>
529
- ) : (
530
- <span style={{ fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #8b93a1)' }}>
531
- 设置改动即时生效并同步至工作区文件树
517
+ 配置已自动保存并实时同步
532
518
  </span>
533
519
  )}
534
520
  </div>
@@ -546,6 +532,90 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
546
532
  )
547
533
  }
548
534
 
535
+ /* ── High-Performance Flattened Visible Row Model ── */
536
+ export interface VisibleFlatRow {
537
+ readonly id: string
538
+ readonly name: string
539
+ readonly path: string
540
+ readonly isDirectory: boolean
541
+ readonly depth: number
542
+ readonly isExpanded: boolean
543
+ readonly hasChildren: boolean
544
+ readonly childCount: number
545
+ readonly size?: number
546
+ readonly item: FileItem
547
+ }
548
+
549
+ /**
550
+ * 线性遍历计算可视行。未展开目录的子节点绝不遍历,彻底解决 DOM 节点爆炸问题。
551
+ */
552
+ function flattenVisibleTree(
553
+ items: FileItem[],
554
+ expandedPaths: Set<string>,
555
+ depth = 0,
556
+ out: VisibleFlatRow[] = [],
557
+ ): VisibleFlatRow[] {
558
+ for (const it of items) {
559
+ const isDir = it.isDirectory
560
+ const hasChildren = isDir && Array.isArray(it.children) && it.children.length > 0
561
+ const isExpanded = isDir && expandedPaths.has(it.path)
562
+ const childCount = hasChildren ? it.children!.length : 0
563
+
564
+ out.push({
565
+ id: it.path,
566
+ name: it.name,
567
+ path: it.path,
568
+ isDirectory: isDir,
569
+ depth,
570
+ isExpanded,
571
+ hasChildren,
572
+ childCount,
573
+ size: it.size,
574
+ item: it,
575
+ })
576
+
577
+ if (isDir && isExpanded && hasChildren) {
578
+ flattenVisibleTree(it.children!, expandedPaths, depth + 1, out)
579
+ }
580
+ }
581
+ return out
582
+ }
583
+
584
+ /**
585
+ * 树形过滤模式:仅保留包含匹配项的分支并保持展开
586
+ */
587
+ function filterTreeWithAutoExpand(
588
+ nodes: FileItem[],
589
+ q: string,
590
+ ): { filteredItems: FileItem[]; autoExpandPaths: Set<string> } {
591
+ const autoExpand = new Set<string>()
592
+
593
+ function filterNode(node: FileItem): FileItem | null {
594
+ const matchSelf = node.name.toLowerCase().includes(q) || node.path.toLowerCase().includes(q)
595
+ if (node.isDirectory) {
596
+ const children: FileItem[] = []
597
+ for (const child of node.children || []) {
598
+ const c = filterNode(child)
599
+ if (c) children.push(c)
600
+ }
601
+ if (matchSelf || children.length > 0) {
602
+ autoExpand.add(node.path)
603
+ return { ...node, children }
604
+ }
605
+ return null
606
+ }
607
+ return matchSelf ? node : null
608
+ }
609
+
610
+ const result: FileItem[] = []
611
+ for (const it of nodes) {
612
+ const f = filterNode(it)
613
+ if (f) result.push(f)
614
+ }
615
+
616
+ return { filteredItems: result, autoExpandPaths: autoExpand }
617
+ }
618
+
549
619
  /* ── Main Workspace Files View ── */
550
620
  function WorkspaceFilesView({
551
621
  sessionId,
@@ -559,11 +629,15 @@ function WorkspaceFilesView({
559
629
  const [data, setData] = useState<WorkspaceFilesResult | null>(null)
560
630
  const [loading, setLoading] = useState(true)
561
631
  const [error, setError] = useState<string | null>(null)
562
- const [query, setQuery] = useState('')
632
+ const [inputQuery, setInputQuery] = useState('')
563
633
  const [toast, setToast] = useState<string | null>(null)
564
634
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set())
565
635
  const [activeConfig, setActiveConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
566
636
  const [showHidden, setShowHidden] = useState(() => loadWorkspaceFilesConfig().showHidden)
637
+ const [searchViewMode, setSearchViewMode] = useState<'flat' | 'tree'>('flat')
638
+
639
+ // 使用 React 并发特性分离高频打字与重度树过滤
640
+ const deferredQuery = useDeferredValue(inputQuery.trim().toLowerCase())
567
641
 
568
642
  const toastTimer = useRef<number | null>(null)
569
643
 
@@ -573,6 +647,10 @@ function WorkspaceFilesView({
573
647
  toastTimer.current = window.setTimeout(() => setToast(null), 2000)
574
648
  }, [])
575
649
 
650
+ useEffect(() => {
651
+ ensureFilesStyle()
652
+ }, [])
653
+
576
654
  useEffect(() => {
577
655
  const handleCfgChange = (e: any) => {
578
656
  if (e.detail) {
@@ -597,12 +675,10 @@ function WorkspaceFilesView({
597
675
  })
598
676
  setData(res)
599
677
 
600
- // 默认折叠,并从 localStorage 恢复已记录的展开状态
601
678
  setExpandedPaths(prev => {
602
679
  if (prev.size > 0) return prev
603
680
  const stored = loadStoredExpandedPaths(res.root || cwd)
604
681
  if (stored !== null) return stored
605
- // 默认全部关闭,不展开任何目录
606
682
  return new Set<string>()
607
683
  })
608
684
  } catch (err: any) {
@@ -616,7 +692,7 @@ function WorkspaceFilesView({
616
692
  void loadTree()
617
693
  }, [loadTree])
618
694
 
619
- const handleToggleExpand = (path: string) => {
695
+ const handleToggleExpand = useCallback((path: string) => {
620
696
  setExpandedPaths(prev => {
621
697
  const next = new Set(prev)
622
698
  if (next.has(path)) next.delete(path)
@@ -624,9 +700,9 @@ function WorkspaceFilesView({
624
700
  saveStoredExpandedPaths(data?.root || cwd, next)
625
701
  return next
626
702
  })
627
- }
703
+ }, [data?.root, cwd])
628
704
 
629
- const handleExpandAll = () => {
705
+ const handleExpandAll = useCallback(() => {
630
706
  if (!data) return
631
707
  const all = new Set<string>()
632
708
  function collect(items: FileItem[]) {
@@ -640,16 +716,16 @@ function WorkspaceFilesView({
640
716
  collect(data.items)
641
717
  setExpandedPaths(all)
642
718
  saveStoredExpandedPaths(data.root || cwd, all)
643
- }
719
+ }, [data, cwd])
644
720
 
645
- const handleCollapseAll = () => {
721
+ const handleCollapseAll = useCallback(() => {
646
722
  const empty = new Set<string>()
647
723
  setExpandedPaths(empty)
648
724
  saveStoredExpandedPaths(data?.root || cwd, empty)
649
- }
725
+ }, [data?.root, cwd])
650
726
 
651
- // 搜索时点击文件夹:跳回树形视图并展开该目录及全部祖先路径,自动平滑定位
652
- const handleJumpToFolderInTree = (dirPath: string) => {
727
+ // 搜索时点击文件夹:跳回树形视图并展开该目录及全部祖先路径,平滑定位
728
+ const handleJumpToFolderInTree = useCallback((dirPath: string) => {
653
729
  const parts = dirPath.split('/')
654
730
  const toExpand: string[] = []
655
731
  let acc = ''
@@ -668,11 +744,9 @@ function WorkspaceFilesView({
668
744
  return next
669
745
  })
670
746
 
671
- // 退出搜索状态回到树形视图
672
- setQuery('')
747
+ setInputQuery('')
673
748
  showToastMsg(`已定位并展开目录: ${dirPath}/`)
674
749
 
675
- // 平滑滚动定位到该目录节点
676
750
  setTimeout(() => {
677
751
  try {
678
752
  const el = document.querySelector(`[data-tree-path="${dirPath}"]`)
@@ -681,18 +755,16 @@ function WorkspaceFilesView({
681
755
  }
682
756
  } catch {}
683
757
  }, 60)
684
- }
758
+ }, [data?.root, cwd, showToastMsg])
685
759
 
686
- // 复制相对路径
687
- const handleCopyPath = (e: React.MouseEvent, path: string) => {
760
+ const handleCopyPath = useCallback((e: React.MouseEvent, path: string) => {
688
761
  e.stopPropagation()
689
762
  void navigator.clipboard.writeText(path).then(() => {
690
763
  showToastMsg(`已复制路径: ${path}`)
691
764
  })
692
- }
765
+ }, [showToastMsg])
693
766
 
694
- // 一键 @ 引用到当前对话框输入框(优先插入官方原生交互式 Chip 胶囊)
695
- const handleMention = (e: React.MouseEvent, itemOrPath: string | FileItem) => {
767
+ const handleMention = useCallback((e: React.MouseEvent, itemOrPath: string | FileItem) => {
696
768
  e.stopPropagation()
697
769
  const itemObj: FileItem = typeof itemOrPath === 'string'
698
770
  ? { name: itemOrPath.split('/').pop() || itemOrPath, path: itemOrPath, isDirectory: false }
@@ -700,24 +772,17 @@ function WorkspaceFilesView({
700
772
 
701
773
  const mentionText = formatMentionText(itemObj.path, itemObj.isDirectory)
702
774
 
703
- // 1. 优先尝试向会话输入框插入官方原生 ReferenceChipNode 胶囊卡片
704
775
  let inserted = false
705
776
  try {
706
777
  inserted = tryInsertNativeReferenceChip(globalCtx, sessionId, itemObj)
707
- } catch (err) {
708
- console.warn('[dsh-workspace-files] 原生 Chip 插入失败:', err)
709
- }
778
+ } catch {}
710
779
 
711
- // 2. 若原生 Chip 插入未成功,无缝降级为规范纯文本插入
712
780
  if (!inserted) {
713
781
  try {
714
782
  inserted = insertMentionToComposer(mentionText)
715
- } catch (err) {
716
- console.warn('[dsh-workspace-files] 降级纯文本插入失败:', err)
717
- }
783
+ } catch {}
718
784
  }
719
785
 
720
- // 写入剪贴板作为双重保障
721
786
  const displayName = itemObj.isDirectory ? `${itemObj.name}/` : itemObj.name
722
787
  void navigator.clipboard.writeText(mentionText.trim()).then(() => {
723
788
  if (inserted) {
@@ -726,10 +791,9 @@ function WorkspaceFilesView({
726
791
  showToastMsg(`已复制引用: ${mentionText.trim()}`)
727
792
  }
728
793
  })
729
- }
794
+ }, [sessionId, showToastMsg])
730
795
 
731
- // 在文件资源管理器中定位/打开
732
- const handleRevealInExplorer = (e: React.MouseEvent, item: FileItem) => {
796
+ const handleRevealInExplorer = useCallback((e: React.MouseEvent, item: FileItem) => {
733
797
  e.stopPropagation()
734
798
  fetch('/dsh-workspace-files/api/files.open', {
735
799
  method: 'POST',
@@ -753,10 +817,9 @@ function WorkspaceFilesView({
753
817
  .catch(err => {
754
818
  showToastMsg(`请求失败: ${err?.message || String(err)}`)
755
819
  })
756
- }
820
+ }, [sessionId, cwd, showToastMsg])
757
821
 
758
- // 用系统默认应用打开
759
- const handleOpenDefaultApp = (e: React.MouseEvent, item: FileItem) => {
822
+ const handleOpenDefaultApp = useCallback((e: React.MouseEvent, item: FileItem) => {
760
823
  e.stopPropagation()
761
824
  fetch('/dsh-workspace-files/api/files.open', {
762
825
  method: 'POST',
@@ -780,88 +843,49 @@ function WorkspaceFilesView({
780
843
  .catch(err => {
781
844
  showToastMsg(`请求失败: ${err?.message || String(err)}`)
782
845
  })
783
- }
784
-
785
- const [searchViewMode, setSearchViewMode] = useState<'flat' | 'tree'>('flat')
786
- const [searchExpandedPaths, setSearchExpandedPaths] = useState<Set<string>>(() => new Set())
846
+ }, [sessionId, cwd, showToastMsg])
787
847
 
788
- // 过滤树与扁平结果
789
- const { filteredTreeItems, flatSearchResults } = useMemo(() => {
790
- if (!data) return { filteredTreeItems: [], flatSearchResults: [] }
791
- const q = query.trim().toLowerCase()
792
- if (!q) return { filteredTreeItems: data.items, flatSearchResults: [] }
848
+ const isSearchActive = Boolean(deferredQuery)
793
849
 
794
- // 扁平结果列表
795
- const flat: Array<{ item: FileItem; parentDir: string }> = []
796
- function traverseFlat(items: FileItem[], parentDir: string) {
850
+ // 1. 扁平搜索结果计算(限定前 150 项,秒速完成)
851
+ const flatSearchResults = useMemo(() => {
852
+ if (!data || !isSearchActive) return []
853
+ const results: Array<{ item: FileItem; parentDir: string }> = []
854
+ function traverse(items: FileItem[], parentDir: string) {
797
855
  for (const it of items) {
798
- const match = it.name.toLowerCase().includes(q) || it.path.toLowerCase().includes(q)
799
- if (match) {
800
- flat.push({ item: it, parentDir })
856
+ if (it.name.toLowerCase().includes(deferredQuery) || it.path.toLowerCase().includes(deferredQuery)) {
857
+ results.push({ item: it, parentDir })
858
+ if (results.length >= 300) return
801
859
  }
802
860
  if (it.isDirectory && it.children) {
803
- traverseFlat(it.children, it.path)
804
- }
805
- }
806
- }
807
- traverseFlat(data.items, '')
808
-
809
- // 树形过滤集合
810
- function filterNode(node: FileItem): FileItem | null {
811
- const selfMatch = node.name.toLowerCase().includes(q) || node.path.toLowerCase().includes(q)
812
- if (node.isDirectory) {
813
- const filteredChildren: FileItem[] = []
814
- for (const child of node.children || []) {
815
- const matchedChild = filterNode(child)
816
- if (matchedChild) filteredChildren.push(matchedChild)
817
- }
818
- if (selfMatch || filteredChildren.length > 0) {
819
- return { ...node, children: filteredChildren }
861
+ traverse(it.children, it.path)
862
+ if (results.length >= 300) return
820
863
  }
821
- return null
822
864
  }
823
- return selfMatch ? node : null
824
865
  }
825
-
826
- const tree: FileItem[] = []
827
- for (const rootNode of data.items) {
828
- const matched = filterNode(rootNode)
829
- if (matched) tree.push(matched)
830
- }
831
-
832
- return { filteredTreeItems: tree, flatSearchResults: flat }
833
- }, [data, query])
834
-
835
- // 搜索关键字变化时,自动展开匹配的目录树节点,并允许用户后续自由折叠/展开
836
- useEffect(() => {
837
- if (!query.trim() || !data) return
838
- const q = query.trim().toLowerCase()
839
- const autoExpand = new Set<string>()
840
- function collect(items: FileItem[]) {
841
- for (const it of items) {
842
- if (it.isDirectory) {
843
- const selfMatch = it.name.toLowerCase().includes(q) || it.path.toLowerCase().includes(q)
844
- const childMatch = it.children?.some(c => c.name.toLowerCase().includes(q) || c.path.toLowerCase().includes(q))
845
- if (selfMatch || childMatch) autoExpand.add(it.path)
846
- if (it.children) collect(it.children)
847
- }
848
- }
866
+ traverse(data.items, '')
867
+ return results
868
+ }, [data, deferredQuery, isSearchActive])
869
+
870
+ // 2. 扁平可见行计算(常规树与搜索树),O(N_visible) 极速生成
871
+ const visibleFlatRows = useMemo(() => {
872
+ if (!data) return []
873
+ if (!isSearchActive) {
874
+ return flattenVisibleTree(data.items, expandedPaths)
849
875
  }
850
- collect(data.items)
851
- setSearchExpandedPaths(autoExpand)
852
- }, [query, data])
853
876
 
854
- const isSearchActive = Boolean(query.trim())
877
+ // 搜索树模式
878
+ const { filteredItems, autoExpandPaths } = filterTreeWithAutoExpand(data.items, deferredQuery)
879
+ return flattenVisibleTree(filteredItems, autoExpandPaths)
880
+ }, [data, isSearchActive, deferredQuery, expandedPaths])
855
881
 
856
882
  return (
857
883
  <div className="dsh-files-root">
858
- <GlobalFilesStyle />
859
-
860
- {/* Header */}
884
+ {/* 1. Header Banner */}
861
885
  <header className="dsh-files-header">
862
886
  <div className="dsh-files-header-info">
863
887
  <div className="dsh-files-title-row">
864
- <span className="dsh-files-title">文件</span>
888
+ <span className="dsh-files-title">工作区文件</span>
865
889
  {data && (
866
890
  <span className="dsh-files-stats-badge">
867
891
  {data.totalFiles} 文件 · {data.totalDirs} 目录
@@ -874,26 +898,26 @@ function WorkspaceFilesView({
874
898
  </div>
875
899
  </header>
876
900
 
877
- {/* Search & Actions Toolbar */}
901
+ {/* 2. Search Toolbar (Clean Elevated Input + Motion Action Pills) */}
878
902
  <div className="dsh-files-toolbar">
879
903
  <div className="dsh-files-search-wrap">
880
904
  <span className="dsh-files-search-icon"><IconSearch size={13} /></span>
881
905
  <input
882
906
  type="text"
883
907
  className="dsh-files-search-input"
884
- placeholder="搜索文件 (支持路径筛选)..."
885
- value={query}
886
- onChange={e => setQuery(e.target.value)}
908
+ placeholder="搜索文件或路径 (Esc 清空)..."
909
+ value={inputQuery}
910
+ onChange={e => setInputQuery(e.target.value)}
887
911
  onKeyDown={e => {
888
- if (e.key === 'Escape') setQuery('')
912
+ if (e.key === 'Escape') setInputQuery('')
889
913
  }}
890
914
  />
891
- {query && (
915
+ {inputQuery && (
892
916
  <button
893
917
  type="button"
894
918
  className="dsh-files-clear-btn"
895
- onClick={() => setQuery('')}
896
- title="清除搜索"
919
+ onClick={() => setInputQuery('')}
920
+ title="清空搜索"
897
921
  >
898
922
  <IconClose size={12} />
899
923
  </button>
@@ -906,7 +930,7 @@ function WorkspaceFilesView({
906
930
  type="button"
907
931
  className="dsh-files-tool-btn is-active"
908
932
  onClick={() => setSearchViewMode(searchViewMode === 'flat' ? 'tree' : 'flat')}
909
- title={searchViewMode === 'flat' ? '当前为扁平列表,点击切换为树形视图' : '当前为树形视图,点击切换为扁平列表'}
933
+ title={searchViewMode === 'flat' ? '切为树形匹配视图' : '切为扁平列表视图'}
910
934
  >
911
935
  {searchViewMode === 'flat' ? <IconTree size={13} /> : <IconList size={13} />}
912
936
  </button>
@@ -916,7 +940,7 @@ function WorkspaceFilesView({
916
940
  type="button"
917
941
  className="dsh-files-tool-btn"
918
942
  onClick={handleExpandAll}
919
- title="全部展开"
943
+ title="展开全部目录"
920
944
  >
921
945
  <IconExpandAll size={13} />
922
946
  </button>
@@ -924,7 +948,7 @@ function WorkspaceFilesView({
924
948
  type="button"
925
949
  className="dsh-files-tool-btn"
926
950
  onClick={handleCollapseAll}
927
- title="全部折叠"
951
+ title="折叠全部目录"
928
952
  >
929
953
  <IconCollapseAll size={13} />
930
954
  </button>
@@ -935,9 +959,9 @@ function WorkspaceFilesView({
935
959
  type="button"
936
960
  className={`dsh-files-tool-btn ${showHidden ? 'is-active' : ''}`}
937
961
  onClick={() => setShowHidden(!showHidden)}
938
- title={showHidden ? '隐藏点号开头的文件' : '显示点号开头的文件'}
962
+ title={showHidden ? '隐藏以点开头的文件' : '显示以点开头的文件'}
939
963
  >
940
- <span style={{ fontSize: 11, fontWeight: 700 }}>.*</span>
964
+ <span style={{ fontSize: 10.5, fontWeight: 700 }}>.*</span>
941
965
  </button>
942
966
  <button
943
967
  type="button"
@@ -951,7 +975,7 @@ function WorkspaceFilesView({
951
975
  </div>
952
976
  </div>
953
977
 
954
- {/* Error Banner */}
978
+ {/* Error Alert */}
955
979
  {error && (
956
980
  <div className="dsh-files-banner-error">
957
981
  <span>{error}</span>
@@ -961,24 +985,24 @@ function WorkspaceFilesView({
961
985
  </div>
962
986
  )}
963
987
 
964
- {/* Tree / Search Results Content */}
988
+ {/* 3. High-Performance Virtualized-Ready Content */}
965
989
  <div className="dsh-files-body">
966
990
  {loading && !data ? (
967
991
  <div className="dsh-files-centered">
968
992
  <div className="dsh-files-spinner" />
969
- <span>加载文件…</span>
993
+ <span>正在索引工作区文件…</span>
970
994
  </div>
971
995
  ) : isSearchActive && flatSearchResults.length === 0 ? (
972
996
  <div className="dsh-files-empty">
973
- <div className="dsh-files-empty-icon"><IconFolder size={26} /></div>
974
- <div className="dsh-files-empty-title">未搜索到匹配的文件</div>
975
- <div className="dsh-files-empty-detail">请尝试更换搜索关键字,或按 Esc 清除搜索。</div>
997
+ <div className="dsh-files-empty-icon"><IconFolder size={28} /></div>
998
+ <div className="dsh-files-empty-title">未匹配到相关文件</div>
999
+ <div className="dsh-files-empty-detail">请尝试更换关键字或按 Esc 恢复完整目录树。</div>
976
1000
  </div>
977
1001
  ) : !isSearchActive && (!data || data.items.length === 0) ? (
978
1002
  <div className="dsh-files-empty">
979
- <div className="dsh-files-empty-icon"><IconFolder size={26} /></div>
1003
+ <div className="dsh-files-empty-icon"><IconFolder size={28} /></div>
980
1004
  <div className="dsh-files-empty-title">工作区为空</div>
981
- <div className="dsh-files-empty-detail">当前工作目录下暂无可显示的文件。</div>
1005
+ <div className="dsh-files-empty-detail">当前工作目录下暂无可展示的文件。</div>
982
1006
  </div>
983
1007
  ) : isSearchActive && searchViewMode === 'flat' ? (
984
1008
  /* ── 扁平搜索结果列表 ── */
@@ -986,13 +1010,13 @@ function WorkspaceFilesView({
986
1010
  <div className="dsh-files-search-header">
987
1011
  <span className="dsh-files-search-count">
988
1012
  找到 {flatSearchResults.length} 个匹配项
989
- {flatSearchResults.length > 150 ? '(展示前 150 项)' : ''}
1013
+ {flatSearchResults.length > 150 ? '(已展示前 150 项)' : ''}
990
1014
  </span>
991
1015
  <button
992
1016
  type="button"
993
1017
  className="dsh-files-view-toggle-btn"
994
1018
  onClick={() => setSearchViewMode('tree')}
995
- title="切换为可折叠的树形视图"
1019
+ title="切换为树形视图"
996
1020
  >
997
1021
  <IconTree size={12} />
998
1022
  <span>切为树形</span>
@@ -1010,7 +1034,7 @@ function WorkspaceFilesView({
1010
1034
  onOpenFile(item.path)
1011
1035
  }
1012
1036
  }}
1013
- title={item.isDirectory ? `点击退出搜索并定位展开目录: ${item.path}/` : `点击预览打开文件: ${item.path}`}
1037
+ title={item.isDirectory ? `点击定位并展开目录: ${item.path}/` : `点击预览打开: ${item.path}`}
1014
1038
  >
1015
1039
  <span className="dsh-files-flat-icon">
1016
1040
  {item.isDirectory ? <IconFolder size={14} /> : <FileGlyphIcon filename={item.name} size={14} />}
@@ -1018,7 +1042,7 @@ function WorkspaceFilesView({
1018
1042
  <div className="dsh-files-flat-info">
1019
1043
  <div className="dsh-files-flat-top">
1020
1044
  <span className="dsh-files-flat-name">
1021
- <HighlightText text={item.name} query={query} />
1045
+ <HighlightText text={item.name} query={deferredQuery} />
1022
1046
  {item.isDirectory && '/'}
1023
1047
  </span>
1024
1048
  {item.size !== undefined && (
@@ -1039,16 +1063,16 @@ function WorkspaceFilesView({
1039
1063
  type="button"
1040
1064
  className="dsh-files-action-btn dsh-files-action-at"
1041
1065
  onClick={e => handleMention(e, item)}
1042
- title={`一键 @ 引用此${item.isDirectory ? '目录' : '文件'}到当前会话`}
1066
+ title={`@ 引用${item.isDirectory ? '目录' : '文件'}到会话`}
1043
1067
  >
1044
- <IconAt size={13} />
1068
+ <IconAt size={12} />
1045
1069
  <span className="dsh-files-action-at-text">@引用</span>
1046
1070
  </button>
1047
1071
  <button
1048
1072
  type="button"
1049
1073
  className="dsh-files-action-btn"
1050
1074
  onClick={e => handleRevealInExplorer(e, item)}
1051
- title={`在文件资源管理器中显示此${item.isDirectory ? '目录' : '文件'}`}
1075
+ title="在文件资源管理器中显示"
1052
1076
  >
1053
1077
  <IconExplorer size={12} />
1054
1078
  </button>
@@ -1056,7 +1080,7 @@ function WorkspaceFilesView({
1056
1080
  type="button"
1057
1081
  className="dsh-files-action-btn"
1058
1082
  onClick={e => handleOpenDefaultApp(e, item)}
1059
- title={`用系统默认应用打开此${item.isDirectory ? '目录' : '文件'}`}
1083
+ title="用系统默认应用打开"
1060
1084
  >
1061
1085
  <IconExternalApp size={12} />
1062
1086
  </button>
@@ -1071,19 +1095,14 @@ function WorkspaceFilesView({
1071
1095
  </div>
1072
1096
  </div>
1073
1097
  ))}
1074
- {flatSearchResults.length > 150 && (
1075
- <div style={{ padding: '10px 14px', fontSize: 11, textAlign: 'center', color: 'var(--dsw-alias-label-tertiary, #8b93a1)', borderTop: '1px dashed var(--dsw-alias-border-l2, #e5e7eb)' }}>
1076
- 匹配项较多,已限制展示前 150 项以保证流畅度,请输入更详细关键字
1077
- </div>
1078
- )}
1079
1098
  </div>
1080
1099
  </div>
1081
1100
  ) : (
1082
- /* ── 树形视图(普通模式或搜索树模式) ── */
1101
+ /* ── 扁平单层映射可视树(高帧率 0 递归开销) ── */
1083
1102
  <div className="dsh-files-tree">
1084
1103
  {isSearchActive && (
1085
1104
  <div className="dsh-files-search-header">
1086
- <span className="dsh-files-search-count">树形匹配(自由折叠/展开)</span>
1105
+ <span className="dsh-files-search-count">树形匹配(共 {visibleFlatRows.length} 项)</span>
1087
1106
  <button
1088
1107
  type="button"
1089
1108
  className="dsh-files-view-toggle-btn"
@@ -1095,38 +1114,24 @@ function WorkspaceFilesView({
1095
1114
  </button>
1096
1115
  </div>
1097
1116
  )}
1098
- {filteredTreeItems.map(item => (
1099
- <FileTreeNode
1100
- key={item.path}
1101
- item={item}
1102
- depth={0}
1103
- query={query}
1104
- isSearchMode={isSearchActive}
1105
- expandedPaths={isSearchActive ? searchExpandedPaths : expandedPaths}
1106
- onToggleExpand={(path: string) => {
1107
- if (isSearchActive) {
1108
- setSearchExpandedPaths(prev => {
1109
- const next = new Set(prev)
1110
- if (next.has(path)) next.delete(path)
1111
- else next.add(path)
1112
- return next
1113
- })
1114
- } else {
1115
- handleToggleExpand(path)
1116
- }
1117
- }}
1117
+ {visibleFlatRows.map(row => (
1118
+ <FileTreeRow
1119
+ key={row.id}
1120
+ row={row}
1121
+ query={deferredQuery}
1122
+ onToggleExpand={handleToggleExpand}
1118
1123
  onOpenFile={onOpenFile}
1119
1124
  onMention={handleMention}
1120
- onCopyPath={handleCopyPath}
1121
1125
  onRevealInExplorer={handleRevealInExplorer}
1122
1126
  onOpenDefaultApp={handleOpenDefaultApp}
1127
+ onCopyPath={handleCopyPath}
1123
1128
  />
1124
1129
  ))}
1125
1130
  </div>
1126
1131
  )}
1127
1132
  </div>
1128
1133
 
1129
- {/* Toast Notification */}
1134
+ {/* Floating Animated Toast */}
1130
1135
  {toast && (
1131
1136
  <div className="dsh-files-toast">
1132
1137
  <IconCheck size={12} />
@@ -1137,165 +1142,141 @@ function WorkspaceFilesView({
1137
1142
  )
1138
1143
  }
1139
1144
 
1140
- /* ── Tree Node Component ── */
1141
- function FileTreeNode(props: {
1142
- item: FileItem
1143
- depth: number
1145
+ /* ── Memoized Flat Tree Row Component ── */
1146
+ interface FileTreeRowProps {
1147
+ row: VisibleFlatRow
1144
1148
  query: string
1145
- isSearchMode: boolean
1146
- expandedPaths: Set<string>
1147
1149
  onToggleExpand: (path: string) => void
1148
1150
  onOpenFile: (path: string) => void
1149
1151
  onMention: (e: React.MouseEvent, item: FileItem) => void
1150
- onCopyPath: (e: React.MouseEvent, path: string) => void
1151
1152
  onRevealInExplorer: (e: React.MouseEvent, item: FileItem) => void
1152
1153
  onOpenDefaultApp: (e: React.MouseEvent, item: FileItem) => void
1153
- }): JSX.Element {
1154
+ onCopyPath: (e: React.MouseEvent, path: string) => void
1155
+ }
1156
+
1157
+ const FileTreeRow = memo(function FileTreeRow(props: FileTreeRowProps): JSX.Element {
1154
1158
  const {
1155
- item,
1156
- depth,
1159
+ row,
1157
1160
  query,
1158
- isSearchMode,
1159
- expandedPaths,
1160
1161
  onToggleExpand,
1161
1162
  onOpenFile,
1162
1163
  onMention,
1163
- onCopyPath,
1164
1164
  onRevealInExplorer,
1165
1165
  onOpenDefaultApp,
1166
+ onCopyPath,
1166
1167
  } = props
1167
- const isExpanded = expandedPaths.has(item.path)
1168
1168
 
1169
- if (item.isDirectory) {
1170
- const childCount = item.children?.length ?? 0
1169
+ const indentPx = row.depth * 14 + 8
1170
+
1171
+ if (row.isDirectory) {
1171
1172
  return (
1172
- <div className="dsh-files-dir-block">
1173
- <div
1174
- className="dsh-files-dir-row"
1175
- data-tree-path={item.path}
1176
- style={{ paddingLeft: `${depth * 14 + 10}px` }}
1177
- onClick={() => onToggleExpand(item.path)}
1178
- >
1179
- <span className="dsh-files-chevron" data-collapsed={!isExpanded ? 'true' : undefined}>▾</span>
1180
- <span className="dsh-files-folder-icon">
1181
- <IconFolder size={14} open={isExpanded} />
1182
- </span>
1183
- <span className="dsh-files-folder-name" title={item.path}>
1184
- <HighlightText text={item.name} query={query} />
1185
- </span>
1186
- <span className="dsh-files-count-badge">{childCount}</span>
1187
-
1188
- <div className="dsh-files-row-actions">
1189
- <button
1190
- type="button"
1191
- className="dsh-files-action-btn dsh-files-action-at"
1192
- onClick={e => onMention(e, item)}
1193
- title="一键 @ 引用此目录到当前会话"
1194
- >
1195
- <IconAt size={13} />
1196
- <span className="dsh-files-action-at-text">@引用</span>
1197
- </button>
1198
- <button
1199
- type="button"
1200
- className="dsh-files-action-btn"
1201
- onClick={e => onRevealInExplorer(e, item)}
1202
- title="在文件资源管理器中打开此目录"
1203
- >
1204
- <IconExplorer size={12} />
1205
- </button>
1206
- <button
1207
- type="button"
1208
- className="dsh-files-action-btn"
1209
- onClick={e => onOpenDefaultApp(e, item)}
1210
- title="用系统默认应用打开此目录"
1211
- >
1212
- <IconExternalApp size={12} />
1213
- </button>
1214
- <button
1215
- type="button"
1216
- className="dsh-files-action-btn"
1217
- onClick={e => onCopyPath(e, item.path)}
1218
- title="复制目录路径"
1219
- >
1220
- <IconCopy size={12} />
1221
- </button>
1222
- </div>
1173
+ <div
1174
+ className="dsh-files-dir-row"
1175
+ data-tree-path={row.path}
1176
+ style={{ paddingLeft: `${indentPx}px` }}
1177
+ onClick={() => onToggleExpand(row.path)}
1178
+ >
1179
+ <span className={`dsh-files-chevron-wrap ${row.isExpanded ? 'is-expanded' : ''}`}>
1180
+ <IconChevronRight size={11} />
1181
+ </span>
1182
+ <span className="dsh-files-folder-icon">
1183
+ <IconFolder size={14} open={row.isExpanded} />
1184
+ </span>
1185
+ <span className="dsh-files-folder-name" title={row.path}>
1186
+ <HighlightText text={row.name} query={query} />
1187
+ </span>
1188
+ <span className="dsh-files-count-badge">{row.childCount}</span>
1189
+
1190
+ <div className="dsh-files-row-actions">
1191
+ <button
1192
+ type="button"
1193
+ className="dsh-files-action-btn dsh-files-action-at"
1194
+ onClick={e => onMention(e, row.item)}
1195
+ title="一键 @ 引用此目录到会话"
1196
+ >
1197
+ <IconAt size={12} />
1198
+ <span className="dsh-files-action-at-text">@引用</span>
1199
+ </button>
1200
+ <button
1201
+ type="button"
1202
+ className="dsh-files-action-btn"
1203
+ onClick={e => onRevealInExplorer(e, row.item)}
1204
+ title="在文件资源管理器中打开此目录"
1205
+ >
1206
+ <IconExplorer size={12} />
1207
+ </button>
1208
+ <button
1209
+ type="button"
1210
+ className="dsh-files-action-btn"
1211
+ onClick={e => onOpenDefaultApp(e, row.item)}
1212
+ title="用系统默认应用打开此目录"
1213
+ >
1214
+ <IconExternalApp size={12} />
1215
+ </button>
1216
+ <button
1217
+ type="button"
1218
+ className="dsh-files-action-btn"
1219
+ onClick={e => onCopyPath(e, row.path)}
1220
+ title="复制目录路径"
1221
+ >
1222
+ <IconCopy size={12} />
1223
+ </button>
1223
1224
  </div>
1224
- {isExpanded && item.children && (
1225
- <div className="dsh-files-children">
1226
- {item.children.map(child => (
1227
- <FileTreeNode
1228
- key={child.path}
1229
- item={child}
1230
- depth={depth + 1}
1231
- query={query}
1232
- isSearchMode={isSearchMode}
1233
- expandedPaths={expandedPaths}
1234
- onToggleExpand={onToggleExpand}
1235
- onOpenFile={onOpenFile}
1236
- onMention={onMention}
1237
- onCopyPath={onCopyPath}
1238
- onRevealInExplorer={onRevealInExplorer}
1239
- onOpenDefaultApp={onOpenDefaultApp}
1240
- />
1241
- ))}
1242
- </div>
1243
- )}
1244
1225
  </div>
1245
1226
  )
1246
1227
  }
1247
1228
 
1248
- // File item row
1249
1229
  return (
1250
1230
  <div
1251
1231
  className="dsh-files-row"
1252
- data-tree-path={item.path}
1253
- style={{ paddingLeft: `${depth * 14 + 10}px` }}
1254
- onClick={() => onOpenFile(item.path)}
1255
- title={`点击打开: ${item.path}`}
1232
+ data-tree-path={row.path}
1233
+ style={{ paddingLeft: `${indentPx}px` }}
1234
+ onClick={() => onOpenFile(row.path)}
1235
+ title={`点击打开文件: ${row.path}`}
1256
1236
  >
1257
- <FileGlyphIcon filename={item.name} size={14} />
1237
+ <span className="dsh-files-file-glyph">
1238
+ <FileGlyphIcon filename={row.name} size={14} />
1239
+ </span>
1258
1240
  <span className="dsh-files-name">
1259
- <HighlightText text={item.name} query={query} />
1241
+ <HighlightText text={row.name} query={query} />
1260
1242
  </span>
1261
1243
 
1262
- {item.size !== undefined && (
1263
- <span className="dsh-files-size" title={`大小: ${item.size} 字节`}>
1264
- {formatFileSize(item.size)}
1244
+ {row.size !== undefined && (
1245
+ <span className="dsh-files-size" title={`大小: ${row.size} 字节`}>
1246
+ {formatFileSize(row.size)}
1265
1247
  </span>
1266
1248
  )}
1267
1249
 
1268
- {/* Row Hover Actions */}
1269
1250
  <div className="dsh-files-row-actions">
1270
1251
  <button
1271
1252
  type="button"
1272
1253
  className="dsh-files-action-btn dsh-files-action-at"
1273
- onClick={e => onMention(e, item)}
1274
- title="一键 @ 引用此文件到当前会话"
1254
+ onClick={e => onMention(e, row.item)}
1255
+ title="一键 @ 引用此文件到会话"
1275
1256
  >
1276
- <IconAt size={13} />
1257
+ <IconAt size={12} />
1277
1258
  <span className="dsh-files-action-at-text">@引用</span>
1278
1259
  </button>
1279
1260
  <button
1280
1261
  type="button"
1281
1262
  className="dsh-files-action-btn"
1282
- onClick={e => onRevealInExplorer(e, item)}
1283
- title="在文件资源管理器中显示此文件"
1263
+ onClick={e => onRevealInExplorer(e, row.item)}
1264
+ title="在文件资源管理器中定位"
1284
1265
  >
1285
1266
  <IconExplorer size={12} />
1286
1267
  </button>
1287
1268
  <button
1288
1269
  type="button"
1289
1270
  className="dsh-files-action-btn"
1290
- onClick={e => onOpenDefaultApp(e, item)}
1291
- title="用系统默认应用打开此文件"
1271
+ onClick={e => onOpenDefaultApp(e, row.item)}
1272
+ title="用系统默认应用打开"
1292
1273
  >
1293
1274
  <IconExternalApp size={12} />
1294
1275
  </button>
1295
1276
  <button
1296
1277
  type="button"
1297
1278
  className="dsh-files-action-btn"
1298
- onClick={e => onCopyPath(e, item.path)}
1279
+ onClick={e => onCopyPath(e, row.path)}
1299
1280
  title="复制相对路径"
1300
1281
  >
1301
1282
  <IconCopy size={12} />
@@ -1303,18 +1284,25 @@ function FileTreeNode(props: {
1303
1284
  </div>
1304
1285
  </div>
1305
1286
  )
1306
- }
1287
+ }, (prev, next) => {
1288
+ return (
1289
+ prev.row.id === next.row.id &&
1290
+ prev.row.isExpanded === next.row.isExpanded &&
1291
+ prev.row.depth === next.row.depth &&
1292
+ prev.row.childCount === next.row.childCount &&
1293
+ prev.query === next.query
1294
+ )
1295
+ })
1307
1296
 
1308
1297
  /* ── Helpers ── */
1309
1298
  function HighlightText({ text, query }: { text: string; query: string }): JSX.Element {
1310
- const q = query.trim().toLowerCase()
1311
- if (!q) return <span>{text}</span>
1312
- const index = text.toLowerCase().indexOf(q)
1299
+ if (!query) return <span>{text}</span>
1300
+ const index = text.toLowerCase().indexOf(query)
1313
1301
  if (index === -1) return <span>{text}</span>
1314
1302
 
1315
1303
  const before = text.slice(0, index)
1316
- const match = text.slice(index, index + q.length)
1317
- const after = text.slice(index + q.length)
1304
+ const match = text.slice(index, index + query.length)
1305
+ const after = text.slice(index + query.length)
1318
1306
 
1319
1307
  return (
1320
1308
  <span>
@@ -1333,12 +1321,6 @@ function formatFileSize(bytes: number): string {
1333
1321
  return `${mb.toFixed(1)} MB`
1334
1322
  }
1335
1323
 
1336
- /**
1337
- * 按照 DSH 官方规范格式化引用路径:
1338
- * - 纯路径无空格:@path
1339
- * - 路径含空格:@"path"
1340
- * - 目录末尾保留斜杠:@folder/ 或 @"folder/"
1341
- */
1342
1324
  function formatMentionText(path: string, isDirectory = false): string {
1343
1325
  const cleanPath = path.replace(/\\/g, '/').replace(/^\.?\//, '')
1344
1326
  const finalPath = isDirectory ? (cleanPath.endsWith('/') ? cleanPath : `${cleanPath}/`) : cleanPath
@@ -1371,11 +1353,7 @@ function saveStoredExpandedPaths(workspaceRoot: string | undefined, paths: Set<s
1371
1353
  } catch {}
1372
1354
  }
1373
1355
 
1374
- /**
1375
- * 从当前 DOM 的 React Fiber 树或 Cordis RootContext 中提取当前活跃输入框的 SessionInputShell 实例
1376
- */
1377
1356
  function findComposerShell(ctx: Context | null, sessionId?: string): any {
1378
- // 1. 优先从 DOM 的 React Fiber 提取(直接命中当前活跃输入框的组件 props)
1379
1357
  try {
1380
1358
  const composerDom = document.querySelector<HTMLElement>(
1381
1359
  '[data-composer-input="true"], [data-composer-input], [data-composer-card], div[role="textbox"][contenteditable="true"]'
@@ -1400,11 +1378,8 @@ function findComposerShell(ctx: Context | null, sessionId?: string): any {
1400
1378
  }
1401
1379
  }
1402
1380
  }
1403
- } catch (e) {
1404
- console.debug('[dsh-workspace-files] 从 Fiber 查找 keyboard 失败:', e)
1405
- }
1381
+ } catch {}
1406
1382
 
1407
- // 2. 从 Cordis RootContext 查找
1408
1383
  if (ctx) {
1409
1384
  try {
1410
1385
  const root: any = (ctx as any).root || ctx
@@ -1420,30 +1395,22 @@ function findComposerShell(ctx: Context | null, sessionId?: string): any {
1420
1395
  return values[values.length - 1]
1421
1396
  }
1422
1397
  }
1423
- } catch (e) {
1424
- console.debug('[dsh-workspace-files] 从 RootContext 查找 shell 失败:', e)
1425
- }
1398
+ } catch {}
1426
1399
  }
1427
1400
 
1428
1401
  return null
1429
1402
  }
1430
1403
 
1431
- /**
1432
- * 尝试向会话输入框插入原生的 ReferenceChipNode(官方胶囊 Tag 卡片)
1433
- */
1434
1404
  function tryInsertNativeReferenceChip(ctx: Context | null, sessionId: string | undefined, item: FileItem): boolean {
1435
1405
  if (typeof document === 'undefined') return false
1436
1406
  try {
1437
- // 聚焦输入框
1438
1407
  const composerDom = document.querySelector<HTMLElement>(
1439
1408
  '[data-composer-input="true"], [data-composer-input], [data-composer-card] [contenteditable="true"], div[role="textbox"][contenteditable="true"]'
1440
1409
  )
1441
1410
  if (composerDom) composerDom.focus()
1442
1411
 
1443
1412
  const shell = findComposerShell(ctx, sessionId)
1444
- if (!shell) {
1445
- return false
1446
- }
1413
+ if (!shell) return false
1447
1414
 
1448
1415
  const cleanPath = item.path.replace(/\\/g, '/').replace(/^\.?\//, '')
1449
1416
  const mention = item.isDirectory ? (cleanPath.endsWith('/') ? `@${cleanPath}` : `@${cleanPath}/`) : `@${cleanPath}`
@@ -1462,7 +1429,6 @@ function tryInsertNativeReferenceChip(ctx: Context | null, sessionId: string | u
1462
1429
  let start = caret
1463
1430
  const end = caret
1464
1431
 
1465
- // 智能消除:若当前光标前是 '@',回退一个字符将其吃掉替换
1466
1432
  if (start > 0 && text[start - 1] === '@') {
1467
1433
  start = start - 1
1468
1434
  }
@@ -1473,33 +1439,23 @@ function tryInsertNativeReferenceChip(ctx: Context | null, sessionId: string | u
1473
1439
  end,
1474
1440
  }
1475
1441
 
1476
- // 优先调用原生 shell.insertReference 插入 Chip Tag
1477
1442
  if (typeof shell.insertReference === 'function') {
1478
1443
  const ok = shell.insertReference(reference, span)
1479
1444
  if (ok) return true
1480
1445
  }
1481
1446
 
1482
- // 备用:尝试通过 session actx 派发事件
1483
1447
  const actx = shell.deps?.actx
1484
1448
  if (actx && typeof actx.bail === 'function') {
1485
1449
  const ok = actx.bail(actx, 'slash/input-insert-reference', { reference, span })
1486
1450
  if (ok === true) return true
1487
1451
  }
1488
- } catch (err) {
1489
- console.debug('[dsh-workspace-files] 插入原生 Chip 异常:', err)
1490
- }
1452
+ } catch {}
1491
1453
  return false
1492
1454
  }
1493
1455
 
1494
- /**
1495
- * 将引用文本注入到 DSH 当前会话的主输入框。
1496
- * 兼容 DSH 官方基于 Lexical 的 ComposerContentEditable (div[data-composer-input])
1497
- * 以及传统的 textarea / input,并自动消除用户手动输入的冗余 '@'。
1498
- */
1499
1456
  function insertMentionToComposer(mentionText: string): boolean {
1500
1457
  if (typeof document === 'undefined') return false
1501
1458
 
1502
- // 1. 查找 DSH 官方输入框(Lexical contenteditable 容器优先)
1503
1459
  const candidates = [
1504
1460
  document.querySelector<HTMLElement>('[data-composer-input="true"]'),
1505
1461
  document.querySelector<HTMLElement>('[data-composer-input]'),
@@ -1513,7 +1469,6 @@ function insertMentionToComposer(mentionText: string): boolean {
1513
1469
 
1514
1470
  target.focus()
1515
1471
 
1516
- // 策略 A: contenteditable (DSH 官方 Lexical 编辑器)
1517
1472
  if (target.getAttribute('contenteditable') === 'true' || target.isContentEditable) {
1518
1473
  const sel = window.getSelection()
1519
1474
  if (sel) {
@@ -1527,11 +1482,10 @@ function insertMentionToComposer(mentionText: string): boolean {
1527
1482
  if (!isInTarget) {
1528
1483
  const range = document.createRange()
1529
1484
  range.selectNodeContents(target)
1530
- range.collapse(false) // 光标定位到文本最末尾
1485
+ range.collapse(false)
1531
1486
  sel.removeAllRanges()
1532
1487
  sel.addRange(range)
1533
1488
  } else {
1534
- // 智能消除:若光标前一个字符是已键入的 '@',将其纳入选区进行覆盖替换,避免出现 '@@file'
1535
1489
  const range = sel.getRangeAt(0)
1536
1490
  if (range.collapsed && range.startContainer.nodeType === Node.TEXT_NODE) {
1537
1491
  const nodeText = range.startContainer.textContent || ''
@@ -1545,7 +1499,6 @@ function insertMentionToComposer(mentionText: string): boolean {
1545
1499
  }
1546
1500
  }
1547
1501
 
1548
- // 原生 execCommand('insertText') 会直接触发 Lexical 的 input/beforeinput 事件流并更新 React 状态
1549
1502
  let success = false
1550
1503
  try {
1551
1504
  success = document.execCommand('insertText', false, mentionText)
@@ -1567,13 +1520,11 @@ function insertMentionToComposer(mentionText: string): boolean {
1567
1520
  return success
1568
1521
  }
1569
1522
 
1570
- // 策略 B: 普通 textarea / input
1571
1523
  if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) {
1572
1524
  let start = target.selectionStart ?? target.value.length
1573
1525
  const end = target.selectionEnd ?? target.value.length
1574
1526
  const prev = target.value
1575
1527
 
1576
- // 智能消除:若前面紧随 '@',覆盖它
1577
1528
  if (start > 0 && prev[start - 1] === '@') {
1578
1529
  start = start - 1
1579
1530
  }
@@ -1637,18 +1588,23 @@ async function call<T = any>(method: string, payload: Record<string, unknown> =
1637
1588
  return envelope.value
1638
1589
  }
1639
1590
 
1640
- /* ── CSS Styles ── */
1641
- function GlobalFilesStyle(): JSX.Element {
1642
- return (
1643
- <style>{`
1591
+ /* ── Singleton Dynamic CSS Injection (Zero React re-evaluation) ── */
1592
+ function ensureFilesStyle() {
1593
+ if (typeof document === 'undefined') return
1594
+ const id = 'dsh-workspace-files-core-style'
1595
+ if (document.getElementById(id)) return
1596
+
1597
+ const style = document.createElement('style')
1598
+ style.id = id
1599
+ style.textContent = `
1644
1600
  .dsh-files-root {
1645
1601
  display: flex;
1646
1602
  flex-direction: column;
1647
1603
  height: 100%;
1648
1604
  min-height: 0;
1649
1605
  box-sizing: border-box;
1650
- background: var(--dsw-alias-bg-base, #ffffff);
1651
- color: var(--dsw-alias-label-primary, #17233c);
1606
+ background: var(--dsw-alias-fill-base, #ffffff);
1607
+ color: var(--dsw-alias-label-primary);
1652
1608
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1653
1609
  font-size: 12px;
1654
1610
  line-height: 1.4;
@@ -1664,7 +1620,8 @@ function GlobalFilesStyle(): JSX.Element {
1664
1620
  justify-content: space-between;
1665
1621
  gap: 8px;
1666
1622
  padding: 8px 12px;
1667
- border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(118,137,166,.18));
1623
+ border-bottom: 0.5px solid var(--dsw-alias-border-l1);
1624
+ background: var(--dsw-alias-fill-base, #ffffff);
1668
1625
  }
1669
1626
  .dsh-files-header-info {
1670
1627
  flex: 1;
@@ -1678,33 +1635,34 @@ function GlobalFilesStyle(): JSX.Element {
1678
1635
  .dsh-files-title {
1679
1636
  font-weight: 600;
1680
1637
  font-size: 13px;
1681
- color: var(--dsw-alias-label-primary, inherit);
1638
+ color: var(--dsw-alias-label-primary);
1682
1639
  }
1683
1640
  .dsh-files-stats-badge {
1684
1641
  font-size: 10px;
1685
1642
  font-weight: 500;
1686
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1687
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.1));
1688
- padding: 1px 6px;
1643
+ color: var(--dsw-alias-label-tertiary);
1644
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);
1645
+ padding: 1px 7px;
1689
1646
  border-radius: 99px;
1690
1647
  }
1691
1648
  .dsh-files-root-path {
1692
1649
  font-size: 11px;
1693
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1650
+ color: var(--dsw-alias-label-tertiary);
1694
1651
  overflow: hidden;
1695
1652
  text-overflow: ellipsis;
1696
1653
  white-space: nowrap;
1697
- margin-top: 1px;
1654
+ margin-top: 2px;
1698
1655
  }
1699
1656
 
1700
- /* ── Toolbar & Search ── */
1657
+ /* ── Toolbar & Pure White Input ── */
1701
1658
  .dsh-files-toolbar {
1702
1659
  display: flex;
1703
1660
  align-items: center;
1704
1661
  gap: 6px;
1705
- padding: 6px 8px 6px 12px;
1706
- border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(118,137,166,.15));
1707
- background: var(--dsw-alias-bg-base, #ffffff);
1662
+ padding: 6px 10px;
1663
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
1664
+ background: var(--dsw-alias-fill-base, #ffffff);
1665
+ flex-shrink: 0;
1708
1666
  }
1709
1667
  .dsh-files-search-wrap {
1710
1668
  position: relative;
@@ -1715,70 +1673,77 @@ function GlobalFilesStyle(): JSX.Element {
1715
1673
  }
1716
1674
  .dsh-files-search-icon {
1717
1675
  position: absolute;
1718
- left: 7px;
1719
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1676
+ left: 8px;
1677
+ color: var(--dsw-alias-label-tertiary);
1720
1678
  pointer-events: none;
1721
1679
  display: flex;
1722
1680
  }
1723
1681
  .dsh-files-search-input {
1724
1682
  box-sizing: border-box;
1725
1683
  width: 100%;
1726
- height: 25px;
1727
- padding: 0 22px 0 26px;
1728
- border: 1px solid var(--dsw-alias-border-l2, rgba(118,137,166,.25));
1729
- border-radius: 4px;
1730
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.06));
1731
- color: inherit;
1732
- font-size: 11.5px;
1684
+ height: 28px;
1685
+ padding: 0 24px 0 28px;
1686
+ border: 0.5px solid var(--dsw-alias-border-l1);
1687
+ border-radius: var(--dsw-radius-sm, 6px);
1688
+ background: var(--dsw-alias-fill-base, #ffffff);
1689
+ color: var(--dsw-alias-label-primary);
1690
+ font-size: 11px;
1733
1691
  outline: none;
1734
- transition: all 0.15s ease;
1692
+ transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
1735
1693
  }
1736
1694
  .dsh-files-search-input:focus {
1737
- background: var(--dsw-alias-bg-base, #ffffff);
1738
- border-color: var(--dsw-alias-brand-primary, #4b70e2);
1739
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 15%, transparent);
1695
+ border-color: var(--dsw-alias-state-business-primary);
1696
+ background: var(--dsw-alias-fill-base, #ffffff);
1697
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
1740
1698
  }
1741
1699
  .dsh-files-clear-btn {
1742
1700
  position: absolute;
1743
- right: 5px;
1701
+ right: 6px;
1744
1702
  border: none;
1745
1703
  background: transparent;
1746
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1704
+ color: var(--dsw-alias-label-tertiary);
1747
1705
  cursor: pointer;
1748
1706
  padding: 2px;
1749
1707
  display: flex;
1708
+ border-radius: 50%;
1709
+ transition: all 0.12s ease;
1750
1710
  }
1751
1711
  .dsh-files-clear-btn:hover {
1752
- color: var(--dsw-alias-label-primary, inherit);
1712
+ color: var(--dsw-alias-label-primary);
1713
+ background: var(--dsw-alias-interactive-bg-hover);
1753
1714
  }
1754
1715
  .dsh-files-toolbar-btns {
1755
1716
  display: flex;
1756
1717
  align-items: center;
1757
1718
  gap: 2px;
1719
+ flex-shrink: 0;
1758
1720
  }
1759
1721
  .dsh-files-tool-btn {
1760
1722
  display: inline-flex;
1761
1723
  align-items: center;
1762
1724
  justify-content: center;
1763
- width: 24px;
1764
- height: 24px;
1725
+ width: 26px;
1726
+ height: 26px;
1765
1727
  border: none;
1766
- border-radius: 4px;
1728
+ border-radius: var(--dsw-radius-xs, 4px);
1767
1729
  background: transparent;
1768
- color: var(--dsw-alias-label-secondary, #6f7f9b);
1730
+ color: var(--dsw-alias-label-secondary);
1769
1731
  cursor: pointer;
1770
- transition: all 0.12s ease;
1732
+ transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1);
1771
1733
  }
1772
1734
  .dsh-files-tool-btn:hover:not(:disabled) {
1773
- color: var(--dsw-alias-label-primary, inherit);
1774
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.1));
1735
+ color: var(--dsw-alias-label-primary);
1736
+ background: var(--dsw-alias-interactive-bg-hover);
1737
+ }
1738
+ .dsh-files-tool-btn:active:not(:disabled) {
1739
+ transform: scale(0.94);
1775
1740
  }
1776
1741
  .dsh-files-tool-btn.is-active {
1777
- color: var(--dsw-alias-brand-primary, #4b70e2);
1778
- background: var(--dsw-alias-interactive-bg-active, rgba(75,112,226,.15));
1742
+ color: var(--dsw-alias-state-business-primary);
1743
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent);
1779
1744
  }
1780
1745
  .dsh-files-tool-btn:disabled {
1781
- opacity: 0.4;
1746
+ opacity: 0.35;
1782
1747
  cursor: not-allowed;
1783
1748
  }
1784
1749
 
@@ -1790,10 +1755,8 @@ function GlobalFilesStyle(): JSX.Element {
1790
1755
  overflow-x: hidden;
1791
1756
  padding: 4px 0;
1792
1757
  }
1793
- .dsh-files-dir-block {
1794
- display: flex;
1795
- flex-direction: column;
1796
- }
1758
+
1759
+ /* ── Directory Row (with Motion Chevron) ── */
1797
1760
  .dsh-files-dir-row {
1798
1761
  display: flex;
1799
1762
  align-items: center;
@@ -1803,29 +1766,35 @@ function GlobalFilesStyle(): JSX.Element {
1803
1766
  cursor: pointer;
1804
1767
  user-select: none;
1805
1768
  position: relative;
1806
- color: var(--dsw-alias-label-secondary, #6f7f9b);
1769
+ color: var(--dsw-alias-label-primary);
1770
+ border-radius: var(--dsw-radius-xs, 4px);
1771
+ margin: 0 4px;
1807
1772
  transition: background 0.12s ease;
1808
1773
  }
1809
1774
  .dsh-files-dir-row:hover {
1810
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.08));
1811
- color: var(--dsw-alias-label-primary, inherit);
1775
+ background: var(--dsw-alias-interactive-bg-hover);
1812
1776
  }
1813
- .dsh-files-chevron {
1814
- font-size: 10px;
1815
- transition: transform 0.15s ease;
1816
- display: inline-block;
1817
- opacity: 0.8;
1777
+ .dsh-files-chevron-wrap {
1778
+ width: 12px;
1779
+ height: 12px;
1780
+ display: inline-flex;
1781
+ align-items: center;
1782
+ justify-content: center;
1783
+ color: var(--dsw-alias-label-tertiary);
1784
+ transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
1785
+ flex-shrink: 0;
1818
1786
  }
1819
- .dsh-files-chevron[data-collapsed='true'] {
1820
- transform: rotate(-90deg);
1787
+ .dsh-files-chevron-wrap.is-expanded {
1788
+ transform: rotate(90deg);
1821
1789
  }
1822
1790
  .dsh-files-folder-icon {
1823
1791
  display: flex;
1792
+ flex-shrink: 0;
1824
1793
  }
1825
1794
  .dsh-files-folder-name {
1826
1795
  flex: 1;
1827
1796
  min-width: 0;
1828
- font-weight: 500;
1797
+ font-weight: 550;
1829
1798
  font-size: 12px;
1830
1799
  overflow: hidden;
1831
1800
  text-overflow: ellipsis;
@@ -1836,8 +1805,8 @@ function GlobalFilesStyle(): JSX.Element {
1836
1805
  font-weight: 600;
1837
1806
  padding: 0 5px;
1838
1807
  border-radius: 99px;
1839
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.1));
1840
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1808
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);
1809
+ color: var(--dsw-alias-label-tertiary);
1841
1810
  }
1842
1811
 
1843
1812
  /* ── File Row ── */
@@ -1850,44 +1819,95 @@ function GlobalFilesStyle(): JSX.Element {
1850
1819
  cursor: pointer;
1851
1820
  user-select: none;
1852
1821
  position: relative;
1822
+ border-radius: var(--dsw-radius-xs, 4px);
1823
+ margin: 0 4px;
1853
1824
  transition: background 0.12s ease;
1854
1825
  }
1855
1826
  .dsh-files-row:hover {
1856
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.08));
1827
+ background: var(--dsw-alias-interactive-bg-hover);
1828
+ }
1829
+ .dsh-files-file-glyph {
1830
+ margin-left: 12px;
1831
+ display: inline-flex;
1832
+ align-items: center;
1833
+ justify-content: center;
1857
1834
  }
1858
1835
  .dsh-files-name {
1859
1836
  flex: 1;
1860
1837
  min-width: 0;
1861
1838
  font-size: 12px;
1862
- color: var(--dsw-alias-label-primary, inherit);
1839
+ color: var(--dsw-alias-label-primary);
1863
1840
  overflow: hidden;
1864
1841
  text-overflow: ellipsis;
1865
1842
  white-space: nowrap;
1866
1843
  }
1867
1844
  .dsh-files-size {
1868
1845
  font-size: 10px;
1869
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1846
+ color: var(--dsw-alias-label-tertiary);
1870
1847
  flex-shrink: 0;
1871
1848
  margin-right: 4px;
1872
1849
  }
1873
1850
 
1874
- /* ── Hover Actions on Rows ── */
1851
+ /* ── Micro-Interaction Hover Action Pills ── */
1875
1852
  .dsh-files-row-actions {
1876
- display: none;
1853
+ display: flex;
1877
1854
  align-items: center;
1878
- gap: 3px;
1855
+ gap: 2px;
1879
1856
  flex-shrink: 0;
1857
+ opacity: 0;
1858
+ transform: translateX(4px);
1859
+ pointer-events: none;
1860
+ transition: opacity 0.15s cubic-bezier(0.16, 1, 0.3, 1), transform 0.15s cubic-bezier(0.16, 1, 0.3, 1);
1861
+ margin-left: auto;
1880
1862
  }
1881
1863
  .dsh-files-row:hover .dsh-files-row-actions,
1882
1864
  .dsh-files-dir-row:hover .dsh-files-row-actions,
1883
1865
  .dsh-files-flat-row:hover .dsh-files-row-actions {
1884
- display: flex;
1866
+ opacity: 1;
1867
+ transform: translateX(0);
1868
+ pointer-events: auto;
1885
1869
  }
1886
1870
  .dsh-files-row:hover .dsh-files-size,
1887
1871
  .dsh-files-dir-row:hover .dsh-files-count-badge,
1888
1872
  .dsh-files-flat-row:hover .dsh-files-size {
1889
1873
  display: none;
1890
1874
  }
1875
+ .dsh-files-action-btn {
1876
+ display: inline-flex;
1877
+ align-items: center;
1878
+ justify-content: center;
1879
+ gap: 3px;
1880
+ height: 20px;
1881
+ padding: 0 5px;
1882
+ border: 0.5px solid var(--dsw-alias-border-l1);
1883
+ border-radius: var(--dsw-radius-xs, 3px);
1884
+ background: var(--dsw-alias-fill-base, #ffffff);
1885
+ color: var(--dsw-alias-label-secondary);
1886
+ cursor: pointer;
1887
+ transition: all 0.12s cubic-bezier(0.16, 1, 0.3, 1);
1888
+ }
1889
+ .dsh-files-action-btn:hover {
1890
+ color: var(--dsw-alias-label-primary);
1891
+ border-color: var(--dsw-alias-state-business-primary);
1892
+ background: var(--dsw-alias-interactive-bg-hover);
1893
+ }
1894
+ .dsh-files-action-btn:active {
1895
+ transform: scale(0.92);
1896
+ }
1897
+ .dsh-files-action-at {
1898
+ color: var(--dsw-alias-state-business-primary);
1899
+ border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 35%, transparent);
1900
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);
1901
+ }
1902
+ .dsh-files-action-at:hover {
1903
+ color: var(--dsw-alias-label-primary-inverted, #ffffff);
1904
+ background: var(--dsw-alias-state-business-primary);
1905
+ border-color: var(--dsw-alias-state-business-primary);
1906
+ }
1907
+ .dsh-files-action-at-text {
1908
+ font-size: 10px;
1909
+ font-weight: 600;
1910
+ }
1891
1911
 
1892
1912
  /* ── Flat Search Result List ── */
1893
1913
  .dsh-files-search-container {
@@ -1900,9 +1920,9 @@ function GlobalFilesStyle(): JSX.Element {
1900
1920
  justify-content: space-between;
1901
1921
  padding: 6px 12px;
1902
1922
  font-size: 11px;
1903
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1904
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 4%, transparent);
1905
- border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(118,137,166,.15));
1923
+ color: var(--dsw-alias-label-tertiary);
1924
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 4%, transparent);
1925
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
1906
1926
  }
1907
1927
  .dsh-files-search-count {
1908
1928
  font-weight: 500;
@@ -1911,20 +1931,20 @@ function GlobalFilesStyle(): JSX.Element {
1911
1931
  display: inline-flex;
1912
1932
  align-items: center;
1913
1933
  gap: 4px;
1914
- border: 1px solid var(--dsw-alias-border-l2, rgba(118,137,166,.25));
1915
- background: var(--dsw-alias-container-bg, var(--dsw-alias-bg-base, #ffffff));
1916
- color: var(--dsw-alias-brand-primary, #4b70e2);
1934
+ border: 0.5px solid var(--dsw-alias-border-l1);
1935
+ background: var(--dsw-alias-fill-base, #ffffff);
1936
+ color: var(--dsw-alias-state-business-primary);
1917
1937
  cursor: pointer;
1918
1938
  font-size: 10.5px;
1919
1939
  font-weight: 500;
1920
1940
  padding: 2px 7px;
1921
- border-radius: 4px;
1922
- transition: all 0.12s ease;
1941
+ border-radius: var(--dsw-radius-xs, 4px);
1942
+ transition: all 0.12s cubic-bezier(0.16, 1, 0.3, 1);
1923
1943
  }
1924
1944
  .dsh-files-view-toggle-btn:hover {
1925
- background: var(--dsw-alias-brand-primary, #4b70e2);
1926
- color: #ffffff;
1927
- border-color: var(--dsw-alias-brand-primary, #4b70e2);
1945
+ background: var(--dsw-alias-state-business-primary);
1946
+ color: var(--dsw-alias-label-primary-inverted, #ffffff);
1947
+ border-color: var(--dsw-alias-state-business-primary);
1928
1948
  }
1929
1949
  .dsh-files-flat-list {
1930
1950
  padding: 4px 0;
@@ -1933,7 +1953,7 @@ function GlobalFilesStyle(): JSX.Element {
1933
1953
  display: flex;
1934
1954
  align-items: center;
1935
1955
  gap: 8px;
1936
- padding: 4px 12px;
1956
+ padding: 5px 12px;
1937
1957
  cursor: pointer;
1938
1958
  user-select: none;
1939
1959
  position: relative;
@@ -1941,7 +1961,7 @@ function GlobalFilesStyle(): JSX.Element {
1941
1961
  min-height: 32px;
1942
1962
  }
1943
1963
  .dsh-files-flat-row:hover {
1944
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.08));
1964
+ background: var(--dsw-alias-interactive-bg-hover);
1945
1965
  }
1946
1966
  .dsh-files-flat-icon {
1947
1967
  display: flex;
@@ -1965,22 +1985,21 @@ function GlobalFilesStyle(): JSX.Element {
1965
1985
  .dsh-files-flat-name {
1966
1986
  font-size: 12px;
1967
1987
  font-weight: 500;
1968
- color: var(--dsw-alias-label-primary, inherit);
1988
+ color: var(--dsw-alias-label-primary);
1969
1989
  overflow: hidden;
1970
1990
  text-overflow: ellipsis;
1971
1991
  white-space: nowrap;
1972
1992
  }
1973
1993
  .dsh-files-flat-parent {
1974
1994
  font-size: 10.5px;
1975
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
1995
+ color: var(--dsw-alias-label-tertiary);
1976
1996
  overflow: hidden;
1977
1997
  text-overflow: ellipsis;
1978
1998
  white-space: nowrap;
1979
- opacity: 0.8;
1980
1999
  }
1981
2000
  .dsh-files-jump-hint {
1982
2001
  font-size: 10px;
1983
- color: var(--dsw-alias-brand-primary, #4b70e2);
2002
+ color: var(--dsw-alias-state-business-primary);
1984
2003
  opacity: 0;
1985
2004
  transition: opacity 0.12s ease;
1986
2005
  margin-right: 6px;
@@ -1988,49 +2007,16 @@ function GlobalFilesStyle(): JSX.Element {
1988
2007
  white-space: nowrap;
1989
2008
  }
1990
2009
  .dsh-files-flat-row:hover .dsh-files-jump-hint {
1991
- opacity: 0.85;
2010
+ opacity: 0.9;
1992
2011
  }
1993
2012
  .dsh-files-flat-row.is-directory:hover {
1994
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 6%, transparent);
1995
- }
1996
- .dsh-files-action-btn {
1997
- display: inline-flex;
1998
- align-items: center;
1999
- justify-content: center;
2000
- gap: 3px;
2001
- height: 20px;
2002
- padding: 0 5px;
2003
- border: 1px solid var(--dsw-alias-border-l2, rgba(118,137,166,.25));
2004
- border-radius: 3px;
2005
- background: var(--dsw-alias-container-bg, var(--dsw-alias-bg-base, #ffffff));
2006
- color: var(--dsw-alias-label-secondary, #6f7f9b);
2007
- cursor: pointer;
2008
- transition: all 0.12s ease;
2009
- }
2010
- .dsh-files-action-btn:hover {
2011
- color: var(--dsw-alias-label-primary, inherit);
2012
- border-color: var(--dsw-alias-border-l3, rgba(118,137,166,.4));
2013
- background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.12));
2014
- }
2015
- .dsh-files-action-at {
2016
- color: var(--dsw-alias-brand-primary, #4b70e2);
2017
- border-color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 30%, transparent);
2018
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 8%, transparent);
2019
- }
2020
- .dsh-files-action-at:hover {
2021
- color: #ffffff;
2022
- background: var(--dsw-alias-brand-primary, #4b70e2);
2023
- border-color: var(--dsw-alias-brand-primary, #4b70e2);
2024
- }
2025
- .dsh-files-action-at-text {
2026
- font-size: 10px;
2027
- font-weight: 600;
2013
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);
2028
2014
  }
2029
2015
 
2030
2016
  /* ── Highlight & Toast ── */
2031
2017
  .dsh-files-highlight {
2032
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 25%, transparent);
2033
- color: var(--dsw-alias-brand-primary, #4b70e2);
2018
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 22%, transparent);
2019
+ color: var(--dsw-alias-state-business-primary);
2034
2020
  font-weight: 600;
2035
2021
  border-radius: 2px;
2036
2022
  padding: 0 1px;
@@ -2044,12 +2030,12 @@ function GlobalFilesStyle(): JSX.Element {
2044
2030
  align-items: center;
2045
2031
  gap: 6px;
2046
2032
  padding: 6px 12px;
2047
- background: var(--dsw-alias-label-primary, #17233c);
2048
- color: var(--dsw-alias-bg-base, #ffffff);
2033
+ background: var(--dsw-alias-label-primary);
2034
+ color: var(--dsw-alias-fill-base, #ffffff);
2049
2035
  font-size: 11px;
2050
2036
  border-radius: 99px;
2051
- box-shadow: 0 4px 16px rgba(0,0,0,.25);
2052
- animation: dsh-files-fade-in 0.15s ease;
2037
+ box-shadow: 0 4px 16px rgba(0,0,0,0.22);
2038
+ animation: dsh-files-fade-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
2053
2039
  z-index: 100;
2054
2040
  pointer-events: none;
2055
2041
  white-space: nowrap;
@@ -2067,13 +2053,13 @@ function GlobalFilesStyle(): JSX.Element {
2067
2053
  justify-content: center;
2068
2054
  height: 180px;
2069
2055
  gap: 10px;
2070
- color: var(--dsw-alias-label-secondary, #6f7f9b);
2056
+ color: var(--dsw-alias-label-secondary);
2071
2057
  }
2072
2058
  .dsh-files-spinner {
2073
2059
  width: 18px;
2074
2060
  height: 18px;
2075
- border: 2px solid var(--dsw-alias-border-l2, rgba(118,137,166,.3));
2076
- border-top-color: var(--dsw-alias-brand-primary, #4b70e2);
2061
+ border: 2px solid var(--dsw-alias-border-l1);
2062
+ border-top-color: var(--dsw-alias-state-business-primary);
2077
2063
  border-radius: 50%;
2078
2064
  animation: dsh-files-spin 0.8s linear infinite;
2079
2065
  }
@@ -2087,7 +2073,7 @@ function GlobalFilesStyle(): JSX.Element {
2087
2073
  justify-content: center;
2088
2074
  padding: 36px 16px;
2089
2075
  text-align: center;
2090
- color: var(--dsw-alias-label-secondary, #6f7f9b);
2076
+ color: var(--dsw-alias-label-secondary);
2091
2077
  }
2092
2078
  .dsh-files-empty-icon {
2093
2079
  margin-bottom: 8px;
@@ -2100,7 +2086,7 @@ function GlobalFilesStyle(): JSX.Element {
2100
2086
  }
2101
2087
  .dsh-files-empty-detail {
2102
2088
  font-size: 11.5px;
2103
- color: var(--dsw-alias-label-tertiary, #8c9ba5);
2089
+ color: var(--dsw-alias-label-tertiary);
2104
2090
  }
2105
2091
  .dsh-files-banner-error {
2106
2092
  display: flex;
@@ -2109,9 +2095,9 @@ function GlobalFilesStyle(): JSX.Element {
2109
2095
  gap: 8px;
2110
2096
  padding: 6px 12px;
2111
2097
  font-size: 11.5px;
2112
- color: var(--dsw-alias-state-error-primary, #e34c59);
2113
- background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e34c59) 10%, transparent);
2114
- border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(118,137,166,.2));
2098
+ color: var(--dsw-alias-state-danger-primary, #e34c59);
2099
+ background: color-mix(in srgb, var(--dsw-alias-state-danger-primary, #e34c59) 10%, transparent);
2100
+ border-bottom: 0.5px solid var(--dsw-alias-border-l1);
2115
2101
  }
2116
2102
  .dsh-files-banner-close {
2117
2103
  border: none;
@@ -2119,6 +2105,6 @@ function GlobalFilesStyle(): JSX.Element {
2119
2105
  color: inherit;
2120
2106
  cursor: pointer;
2121
2107
  }
2122
- `}</style>
2123
- )
2108
+ `
2109
+ document.head.appendChild(style)
2124
2110
  }