@civilization/dsh-workspace-files 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,10 +8,13 @@ import {
8
8
  FileGlyphIcon,
9
9
  IconAt,
10
10
  IconCheck,
11
+ IconChevronDown,
11
12
  IconClose,
12
13
  IconCollapseAll,
13
14
  IconCopy,
14
15
  IconExpandAll,
16
+ IconExplorer,
17
+ IconExternalApp,
15
18
  IconFolder,
16
19
  IconList,
17
20
  IconRefresh,
@@ -111,6 +114,436 @@ export function apply(ctx: Context): void {
111
114
  key: OFFICIAL_FILES_TAB_ID,
112
115
  }, WorkspaceFilesTabTitle)), 'dsh-workspace-files: official tab title')
113
116
  }
117
+
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
+
133
+ if (slots) {
134
+ registerSettingsCard(ctx)
135
+ }
136
+ if ((ctx as any).inject) {
137
+ (ctx as any).inject(['settingsScope'], (scoped: any) => {
138
+ registerSettingsCard(scoped)
139
+ })
140
+ }
141
+ }
142
+
143
+ /* ── Workspace Files Configuration Store ── */
144
+ export interface WorkspaceFilesConfig {
145
+ maxDepth: number
146
+ showHidden: boolean
147
+ }
148
+
149
+ const CONFIG_STORAGE_KEY = 'dsh_workspace_files_config'
150
+
151
+ export function loadWorkspaceFilesConfig(): WorkspaceFilesConfig {
152
+ try {
153
+ const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(CONFIG_STORAGE_KEY) : null
154
+ if (raw) {
155
+ const parsed = JSON.parse(raw)
156
+ return {
157
+ maxDepth: typeof parsed.maxDepth === 'number' && parsed.maxDepth > 0 ? parsed.maxDepth : 16,
158
+ showHidden: Boolean(parsed.showHidden),
159
+ }
160
+ }
161
+ } catch {}
162
+ return { maxDepth: 16, showHidden: false }
163
+ }
164
+
165
+ export function saveWorkspaceFilesConfig(cfg: WorkspaceFilesConfig): void {
166
+ try {
167
+ if (typeof localStorage !== 'undefined') {
168
+ localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(cfg))
169
+ }
170
+ if (typeof window !== 'undefined') {
171
+ window.dispatchEvent(new CustomEvent('dsh-workspace-files:config-change', { detail: cfg }))
172
+ }
173
+ fetch('/dsh-workspace-files/api/config.set', {
174
+ method: 'POST',
175
+ headers: { 'content-type': 'application/json' },
176
+ body: JSON.stringify(cfg),
177
+ }).catch(() => {})
178
+ } catch {}
179
+ }
180
+
181
+ function ensureSettingsStyle() {
182
+ if (typeof document === 'undefined') return
183
+ const id = 'dsh-workspace-files-settings-style'
184
+ if (document.getElementById(id)) return
185
+ const style = document.createElement('style')
186
+ style.id = id
187
+ style.textContent = `
188
+ .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;
192
+ list-style: none;
193
+ transition: border-color .16s, background .16s;
194
+ box-sizing: border-box;
195
+ }
196
+ .dsh-wf-card-open {
197
+ background: var(--dsw-alias-bg-layer-2, #f7f8fa);
198
+ border-color: var(--dsw-alias-label-dimmed, #c8ccd4);
199
+ }
200
+ .dsh-wf-header {
201
+ -webkit-appearance: none;
202
+ appearance: none;
203
+ width: 100%;
204
+ font: inherit;
205
+ color: inherit;
206
+ text-align: left;
207
+ cursor: pointer;
208
+ background: 0 0;
209
+ border: 0;
210
+ border-radius: 12px;
211
+ align-items: center;
212
+ gap: 12px;
213
+ padding: 14px 16px;
214
+ display: flex;
215
+ }
216
+ .dsh-wf-head-text {
217
+ flex-direction: column;
218
+ flex: 1;
219
+ gap: 4px;
220
+ min-width: 0;
221
+ display: flex;
222
+ }
223
+ .dsh-wf-name {
224
+ color: var(--dsw-alias-label-primary, #1f2328);
225
+ font-size: 15px;
226
+ font-weight: 600;
227
+ line-height: 1.4;
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 8px;
231
+ }
232
+ .dsh-wf-version {
233
+ font-size: 12px;
234
+ font-weight: 400;
235
+ color: var(--dsw-alias-label-tertiary, #8b93a1);
236
+ }
237
+ .dsh-wf-desc {
238
+ color: var(--dsw-alias-label-tertiary, #8b93a1);
239
+ font-size: 13px;
240
+ line-height: 1.5;
241
+ }
242
+ .dsh-wf-chevron {
243
+ color: var(--dsw-alias-label-tertiary, #8b93a1);
244
+ flex: none;
245
+ transition: transform .16s ease-in-out;
246
+ display: inline-flex;
247
+ }
248
+ .dsh-wf-chevron-open {
249
+ transform: rotate(180deg);
250
+ }
251
+ .dsh-wf-body {
252
+ border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
253
+ margin: 0 16px;
254
+ padding: 8px 0 12px;
255
+ }
256
+ .dsh-wf-row {
257
+ align-items: center;
258
+ gap: 16px;
259
+ padding: 12px 0;
260
+ display: flex;
261
+ justify-content: space-between;
262
+ }
263
+ .dsh-wf-row + .dsh-wf-row {
264
+ border-top: 1px solid var(--dsw-alias-border-l2, rgba(118, 137, 166, 0.12));
265
+ }
266
+ .dsh-wf-label-box {
267
+ flex-direction: column;
268
+ flex: 1;
269
+ gap: 3px;
270
+ min-width: 0;
271
+ display: flex;
272
+ }
273
+ .dsh-wf-label {
274
+ font-size: 13.5px;
275
+ font-weight: 550;
276
+ color: var(--dsw-alias-label-primary, #1f2328);
277
+ }
278
+ .dsh-wf-hint {
279
+ color: var(--dsw-alias-label-tertiary, #8b93a1);
280
+ font-size: 12px;
281
+ line-height: 18px;
282
+ }
283
+ .dsh-wf-ctrl {
284
+ flex-shrink: 0;
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 8px;
288
+ }
289
+ .dsh-wf-seg {
290
+ border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
291
+ border-radius: 8px;
292
+ gap: 2px;
293
+ padding: 2px;
294
+ display: inline-flex;
295
+ background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.05));
296
+ }
297
+ .dsh-wf-seg-btn {
298
+ font: inherit;
299
+ color: var(--dsw-alias-label-secondary, #6b7280);
300
+ cursor: pointer;
301
+ background: 0 0;
302
+ border: none;
303
+ border-radius: 6px;
304
+ padding: 4px 10px;
305
+ font-size: 12px;
306
+ line-height: 18px;
307
+ transition: all 0.15s;
308
+ }
309
+ .dsh-wf-seg-btn:hover {
310
+ color: var(--dsw-alias-label-primary, #1f2328);
311
+ }
312
+ .dsh-wf-seg-active {
313
+ background: var(--dsw-alias-bg-layer-3, #eef0f4);
314
+ color: var(--dsw-alias-brand-primary, #4f6ef7);
315
+ font-weight: 600;
316
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
317
+ }
318
+ .dsh-wf-input {
319
+ 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;
323
+ text-align: center;
324
+ color: var(--dsw-alias-label-primary, #1f2328);
325
+ font: inherit;
326
+ border-radius: 7px;
327
+ padding: 4px 6px;
328
+ font-size: 12.5px;
329
+ line-height: 18px;
330
+ }
331
+ .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);
334
+ }
335
+ .dsh-wf-toggle {
336
+ position: relative;
337
+ display: inline-block;
338
+ width: 40px;
339
+ height: 22px;
340
+ cursor: pointer;
341
+ }
342
+ .dsh-wf-toggle input {
343
+ opacity: 0;
344
+ width: 0;
345
+ height: 0;
346
+ position: absolute;
347
+ }
348
+ .dsh-wf-toggle-track {
349
+ position: absolute;
350
+ 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;
354
+ }
355
+ .dsh-wf-toggle input:checked + .dsh-wf-toggle-track {
356
+ background-color: var(--dsw-alias-brand-primary, #4f6ef7);
357
+ }
358
+ .dsh-wf-toggle-thumb {
359
+ position: absolute;
360
+ height: 16px;
361
+ width: 16px;
362
+ left: 3px;
363
+ bottom: 3px;
364
+ background-color: white;
365
+ transition: .2s cubic-bezier(.4, 0, .2, 1);
366
+ border-radius: 50%;
367
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
368
+ }
369
+ .dsh-wf-toggle input:checked + .dsh-wf-toggle-track .dsh-wf-toggle-thumb {
370
+ transform: translateX(18px);
371
+ }
372
+ .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));
376
+ display: flex;
377
+ align-items: center;
378
+ justify-content: space-between;
379
+ }
380
+ .dsh-wf-saved-hint {
381
+ font-size: 11.5px;
382
+ color: var(--dsw-alias-state-success-primary, #10b981);
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 4px;
386
+ }
387
+ .dsh-wf-btn-reset {
388
+ 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);
394
+ cursor: pointer;
395
+ transition: all 0.15s;
396
+ }
397
+ .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);
400
+ }
401
+ `
402
+ document.head.appendChild(style)
403
+ }
404
+
405
+ export function WorkspaceFilesSettingsCard(): JSX.Element {
406
+ const [open, setOpen] = useState(false)
407
+ const [config, setConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
408
+ const [savedTick, setSavedTick] = useState(false)
409
+
410
+ useEffect(() => {
411
+ ensureSettingsStyle()
412
+ }, [])
413
+
414
+ const updateConfig = (patch: Partial<WorkspaceFilesConfig>) => {
415
+ const next = { ...config, ...patch }
416
+ setConfig(next)
417
+ saveWorkspaceFilesConfig(next)
418
+ setSavedTick(true)
419
+ setTimeout(() => setSavedTick(false), 2000)
420
+ }
421
+
422
+ const handleDepthSelect = (depth: number) => {
423
+ updateConfig({ maxDepth: depth })
424
+ }
425
+
426
+ const handleDepthInput = (e: React.ChangeEvent<HTMLInputElement>) => {
427
+ const val = parseInt(e.target.value, 10)
428
+ if (!isNaN(val) && val >= 1 && val <= 100) {
429
+ updateConfig({ maxDepth: val })
430
+ }
431
+ }
432
+
433
+ const handleToggleHidden = () => {
434
+ updateConfig({ showHidden: !config.showHidden })
435
+ }
436
+
437
+ const handleReset = () => {
438
+ const def = { maxDepth: 16, showHidden: false }
439
+ setConfig(def)
440
+ saveWorkspaceFilesConfig(def)
441
+ setSavedTick(true)
442
+ setTimeout(() => setSavedTick(false), 2000)
443
+ }
444
+
445
+ return (
446
+ <div className={open ? 'dsh-wf-card dsh-wf-card-open' : 'dsh-wf-card'}>
447
+ <button
448
+ type="button"
449
+ className="dsh-wf-header"
450
+ aria-expanded={open}
451
+ onClick={() => setOpen(!open)}
452
+ >
453
+ <div className="dsh-wf-head-text">
454
+ <div className="dsh-wf-name">
455
+ <span>工作区文件</span>
456
+ <span className="dsh-wf-version">v0.3.0</span>
457
+ </div>
458
+ <div className="dsh-wf-desc">
459
+ 浏览与检索工作区文件树,支持递归目录扫描与一键引用。
460
+ </div>
461
+ </div>
462
+ <span className={open ? 'dsh-wf-chevron dsh-wf-chevron-open' : 'dsh-wf-chevron'}>
463
+ <IconChevronDown size={14} />
464
+ </span>
465
+ </button>
466
+
467
+ {open && (
468
+ <div className="dsh-wf-body">
469
+ <div className="dsh-wf-row">
470
+ <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>
475
+ </div>
476
+ <div className="dsh-wf-ctrl">
477
+ <div className="dsh-wf-seg">
478
+ {[8, 16, 32, 64].map((d) => (
479
+ <button
480
+ key={d}
481
+ type="button"
482
+ className={config.maxDepth === d ? 'dsh-wf-seg-btn dsh-wf-seg-active' : 'dsh-wf-seg-btn'}
483
+ onClick={() => handleDepthSelect(d)}
484
+ >
485
+ {d}
486
+ </button>
487
+ ))}
488
+ </div>
489
+ <input
490
+ type="number"
491
+ min="1"
492
+ max="100"
493
+ className="dsh-wf-input"
494
+ value={config.maxDepth}
495
+ onChange={handleDepthInput}
496
+ title="自定义深度 (1-100)"
497
+ />
498
+ </div>
499
+ </div>
500
+
501
+ <div className="dsh-wf-row">
502
+ <div className="dsh-wf-label-box">
503
+ <div className="dsh-wf-label">默认显示隐藏文件</div>
504
+ <div className="dsh-wf-hint">
505
+ 打开文件树时是否默认展示以点开头的隐藏文件或文件夹(如 .gitignore, .env 等)。
506
+ </div>
507
+ </div>
508
+ <div className="dsh-wf-ctrl">
509
+ <label className="dsh-wf-toggle">
510
+ <input
511
+ type="checkbox"
512
+ checked={config.showHidden}
513
+ onChange={handleToggleHidden}
514
+ />
515
+ <span className="dsh-wf-toggle-track">
516
+ <span className="dsh-wf-toggle-thumb" />
517
+ </span>
518
+ </label>
519
+ </div>
520
+ </div>
521
+
522
+ <div className="dsh-wf-footer">
523
+ <div>
524
+ {savedTick ? (
525
+ <span className="dsh-wf-saved-hint">
526
+ <IconCheck size={12} />
527
+ <span>已保存设置</span>
528
+ </span>
529
+ ) : (
530
+ <span style={{ fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #8b93a1)' }}>
531
+ 设置改动即时生效并同步至工作区文件树
532
+ </span>
533
+ )}
534
+ </div>
535
+ <button
536
+ type="button"
537
+ className="dsh-wf-btn-reset"
538
+ onClick={handleReset}
539
+ >
540
+ 恢复默认
541
+ </button>
542
+ </div>
543
+ </div>
544
+ )}
545
+ </div>
546
+ )
114
547
  }
115
548
 
116
549
  /* ── Main Workspace Files View ── */
@@ -129,7 +562,8 @@ function WorkspaceFilesView({
129
562
  const [query, setQuery] = useState('')
130
563
  const [toast, setToast] = useState<string | null>(null)
131
564
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set())
132
- const [showHidden, setShowHidden] = useState(false)
565
+ const [activeConfig, setActiveConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
566
+ const [showHidden, setShowHidden] = useState(() => loadWorkspaceFilesConfig().showHidden)
133
567
 
134
568
  const toastTimer = useRef<number | null>(null)
135
569
 
@@ -139,11 +573,28 @@ function WorkspaceFilesView({
139
573
  toastTimer.current = window.setTimeout(() => setToast(null), 2000)
140
574
  }, [])
141
575
 
142
- const loadTree = useCallback(async (quiet = false) => {
576
+ useEffect(() => {
577
+ const handleCfgChange = (e: any) => {
578
+ if (e.detail) {
579
+ setActiveConfig(e.detail)
580
+ setShowHidden(Boolean(e.detail.showHidden))
581
+ }
582
+ }
583
+ window.addEventListener('dsh-workspace-files:config-change', handleCfgChange)
584
+ return () => window.removeEventListener('dsh-workspace-files:config-change', handleCfgChange)
585
+ }, [])
586
+
587
+ const loadTree = useCallback(async (quiet = false, forceRefresh = false) => {
143
588
  if (!quiet) setLoading(true)
144
589
  setError(null)
145
590
  try {
146
- const res = await call<WorkspaceFilesResult>('files.tree', { sessionId, cwd, showHidden })
591
+ const res = await call<WorkspaceFilesResult>('files.tree', {
592
+ sessionId,
593
+ cwd,
594
+ showHidden,
595
+ maxDepth: activeConfig.maxDepth,
596
+ forceRefresh,
597
+ })
147
598
  setData(res)
148
599
 
149
600
  // 默认折叠,并从 localStorage 恢复已记录的展开状态
@@ -159,7 +610,7 @@ function WorkspaceFilesView({
159
610
  } finally {
160
611
  if (!quiet) setLoading(false)
161
612
  }
162
- }, [sessionId, cwd, showHidden])
613
+ }, [sessionId, cwd, showHidden, activeConfig.maxDepth])
163
614
 
164
615
  useEffect(() => {
165
616
  void loadTree()
@@ -277,6 +728,60 @@ function WorkspaceFilesView({
277
728
  })
278
729
  }
279
730
 
731
+ // 在文件资源管理器中定位/打开
732
+ const handleRevealInExplorer = (e: React.MouseEvent, item: FileItem) => {
733
+ e.stopPropagation()
734
+ fetch('/dsh-workspace-files/api/files.open', {
735
+ method: 'POST',
736
+ headers: { 'content-type': 'application/json' },
737
+ body: JSON.stringify({
738
+ action: 'reveal',
739
+ path: item.path,
740
+ isDirectory: Boolean(item.isDirectory),
741
+ sessionId,
742
+ cwd,
743
+ }),
744
+ })
745
+ .then(res => res.json())
746
+ .then(data => {
747
+ if (data.ok) {
748
+ showToastMsg(`已在资源管理器中显示: ${item.name}`)
749
+ } else {
750
+ showToastMsg(`打开失败: ${data.error?.message || '未知错误'}`)
751
+ }
752
+ })
753
+ .catch(err => {
754
+ showToastMsg(`请求失败: ${err?.message || String(err)}`)
755
+ })
756
+ }
757
+
758
+ // 用系统默认应用打开
759
+ const handleOpenDefaultApp = (e: React.MouseEvent, item: FileItem) => {
760
+ e.stopPropagation()
761
+ fetch('/dsh-workspace-files/api/files.open', {
762
+ method: 'POST',
763
+ headers: { 'content-type': 'application/json' },
764
+ body: JSON.stringify({
765
+ action: 'openDefault',
766
+ path: item.path,
767
+ isDirectory: Boolean(item.isDirectory),
768
+ sessionId,
769
+ cwd,
770
+ }),
771
+ })
772
+ .then(res => res.json())
773
+ .then(data => {
774
+ if (data.ok) {
775
+ showToastMsg(`已启动系统应用打开: ${item.name}`)
776
+ } else {
777
+ showToastMsg(`打开失败: ${data.error?.message || '未知错误'}`)
778
+ }
779
+ })
780
+ .catch(err => {
781
+ showToastMsg(`请求失败: ${err?.message || String(err)}`)
782
+ })
783
+ }
784
+
280
785
  const [searchViewMode, setSearchViewMode] = useState<'flat' | 'tree'>('flat')
281
786
  const [searchExpandedPaths, setSearchExpandedPaths] = useState<Set<string>>(() => new Set())
282
787
 
@@ -437,7 +942,7 @@ function WorkspaceFilesView({
437
942
  <button
438
943
  type="button"
439
944
  className="dsh-files-tool-btn"
440
- onClick={() => void loadTree(true)}
945
+ onClick={() => void loadTree(false, true)}
441
946
  disabled={loading}
442
947
  title="刷新文件树"
443
948
  >
@@ -479,7 +984,10 @@ function WorkspaceFilesView({
479
984
  /* ── 扁平搜索结果列表 ── */
480
985
  <div className="dsh-files-search-container">
481
986
  <div className="dsh-files-search-header">
482
- <span className="dsh-files-search-count">找到 {flatSearchResults.length} 个匹配项</span>
987
+ <span className="dsh-files-search-count">
988
+ 找到 {flatSearchResults.length} 个匹配项
989
+ {flatSearchResults.length > 150 ? '(展示前 150 项)' : ''}
990
+ </span>
483
991
  <button
484
992
  type="button"
485
993
  className="dsh-files-view-toggle-btn"
@@ -491,7 +999,7 @@ function WorkspaceFilesView({
491
999
  </button>
492
1000
  </div>
493
1001
  <div className="dsh-files-flat-list">
494
- {flatSearchResults.map(({ item, parentDir }) => (
1002
+ {flatSearchResults.slice(0, 150).map(({ item, parentDir }) => (
495
1003
  <div
496
1004
  key={item.path}
497
1005
  className={`dsh-files-flat-row ${item.isDirectory ? 'is-directory' : ''}`}
@@ -536,6 +1044,22 @@ function WorkspaceFilesView({
536
1044
  <IconAt size={13} />
537
1045
  <span className="dsh-files-action-at-text">@引用</span>
538
1046
  </button>
1047
+ <button
1048
+ type="button"
1049
+ className="dsh-files-action-btn"
1050
+ onClick={e => handleRevealInExplorer(e, item)}
1051
+ title={`在文件资源管理器中显示此${item.isDirectory ? '目录' : '文件'}`}
1052
+ >
1053
+ <IconExplorer size={12} />
1054
+ </button>
1055
+ <button
1056
+ type="button"
1057
+ className="dsh-files-action-btn"
1058
+ onClick={e => handleOpenDefaultApp(e, item)}
1059
+ title={`用系统默认应用打开此${item.isDirectory ? '目录' : '文件'}`}
1060
+ >
1061
+ <IconExternalApp size={12} />
1062
+ </button>
539
1063
  <button
540
1064
  type="button"
541
1065
  className="dsh-files-action-btn"
@@ -547,6 +1071,11 @@ function WorkspaceFilesView({
547
1071
  </div>
548
1072
  </div>
549
1073
  ))}
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
+ )}
550
1079
  </div>
551
1080
  </div>
552
1081
  ) : (
@@ -589,6 +1118,8 @@ function WorkspaceFilesView({
589
1118
  onOpenFile={onOpenFile}
590
1119
  onMention={handleMention}
591
1120
  onCopyPath={handleCopyPath}
1121
+ onRevealInExplorer={handleRevealInExplorer}
1122
+ onOpenDefaultApp={handleOpenDefaultApp}
592
1123
  />
593
1124
  ))}
594
1125
  </div>
@@ -617,8 +1148,22 @@ function FileTreeNode(props: {
617
1148
  onOpenFile: (path: string) => void
618
1149
  onMention: (e: React.MouseEvent, item: FileItem) => void
619
1150
  onCopyPath: (e: React.MouseEvent, path: string) => void
1151
+ onRevealInExplorer: (e: React.MouseEvent, item: FileItem) => void
1152
+ onOpenDefaultApp: (e: React.MouseEvent, item: FileItem) => void
620
1153
  }): JSX.Element {
621
- const { item, depth, query, isSearchMode, expandedPaths, onToggleExpand, onOpenFile, onMention, onCopyPath } = props
1154
+ const {
1155
+ item,
1156
+ depth,
1157
+ query,
1158
+ isSearchMode,
1159
+ expandedPaths,
1160
+ onToggleExpand,
1161
+ onOpenFile,
1162
+ onMention,
1163
+ onCopyPath,
1164
+ onRevealInExplorer,
1165
+ onOpenDefaultApp,
1166
+ } = props
622
1167
  const isExpanded = expandedPaths.has(item.path)
623
1168
 
624
1169
  if (item.isDirectory) {
@@ -650,6 +1195,22 @@ function FileTreeNode(props: {
650
1195
  <IconAt size={13} />
651
1196
  <span className="dsh-files-action-at-text">@引用</span>
652
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>
653
1214
  <button
654
1215
  type="button"
655
1216
  className="dsh-files-action-btn"
@@ -674,6 +1235,8 @@ function FileTreeNode(props: {
674
1235
  onOpenFile={onOpenFile}
675
1236
  onMention={onMention}
676
1237
  onCopyPath={onCopyPath}
1238
+ onRevealInExplorer={onRevealInExplorer}
1239
+ onOpenDefaultApp={onOpenDefaultApp}
677
1240
  />
678
1241
  ))}
679
1242
  </div>
@@ -713,6 +1276,22 @@ function FileTreeNode(props: {
713
1276
  <IconAt size={13} />
714
1277
  <span className="dsh-files-action-at-text">@引用</span>
715
1278
  </button>
1279
+ <button
1280
+ type="button"
1281
+ className="dsh-files-action-btn"
1282
+ onClick={e => onRevealInExplorer(e, item)}
1283
+ title="在文件资源管理器中显示此文件"
1284
+ >
1285
+ <IconExplorer size={12} />
1286
+ </button>
1287
+ <button
1288
+ type="button"
1289
+ className="dsh-files-action-btn"
1290
+ onClick={e => onOpenDefaultApp(e, item)}
1291
+ title="用系统默认应用打开此文件"
1292
+ >
1293
+ <IconExternalApp size={12} />
1294
+ </button>
716
1295
  <button
717
1296
  type="button"
718
1297
  className="dsh-files-action-btn"