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