@civilization/dsh-workspace-files 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/lib/client.js +453 -3
- package/lib/client.js.map +1 -1
- package/lib/index.js +53 -5
- package/package.json +8 -2
- package/src/client/icons.tsx +8 -0
- package/src/client/index.tsx +451 -3
- package/src/files.ts +1 -1
- package/src/index.ts +58 -3
package/src/client/icons.tsx
CHANGED
|
@@ -159,3 +159,11 @@ export function IconTree({ size = 13 }: { size?: number }): JSX.Element {
|
|
|
159
159
|
)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
export function IconChevronDown({ size = 14 }: { size?: number }): JSX.Element {
|
|
163
|
+
return (
|
|
164
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}>
|
|
165
|
+
<polyline points="4 6 8 10 12 6" />
|
|
166
|
+
</svg>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
package/src/client/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
FileGlyphIcon,
|
|
9
9
|
IconAt,
|
|
10
10
|
IconCheck,
|
|
11
|
+
IconChevronDown,
|
|
11
12
|
IconClose,
|
|
12
13
|
IconCollapseAll,
|
|
13
14
|
IconCopy,
|
|
@@ -111,6 +112,436 @@ export function apply(ctx: Context): void {
|
|
|
111
112
|
key: OFFICIAL_FILES_TAB_ID,
|
|
112
113
|
}, WorkspaceFilesTabTitle)), 'dsh-workspace-files: official tab title')
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
// 注入设置中心“插件配置”卡片(命名空间必须为 workspace-files,与 Host settings 服务注册一致)
|
|
117
|
+
const registerSettingsCard = (targetCtx: any) => {
|
|
118
|
+
if (!targetCtx?.slots) return
|
|
119
|
+
targetCtx.effect?.(() => targetCtx.slots.inject('settings.plugin.item', function* () {
|
|
120
|
+
yield targetCtx.slots.register({
|
|
121
|
+
name: 'settings.plugin.item',
|
|
122
|
+
key: 'workspace-files',
|
|
123
|
+
}, WorkspaceFilesSettingsCard)
|
|
124
|
+
yield targetCtx.slots.register({
|
|
125
|
+
name: 'settings.plugin.item',
|
|
126
|
+
key: '@civilization/dsh-workspace-files',
|
|
127
|
+
}, WorkspaceFilesSettingsCard)
|
|
128
|
+
}), 'dsh-workspace-files: settings card')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (slots) {
|
|
132
|
+
registerSettingsCard(ctx)
|
|
133
|
+
}
|
|
134
|
+
if ((ctx as any).inject) {
|
|
135
|
+
(ctx as any).inject(['settingsScope'], (scoped: any) => {
|
|
136
|
+
registerSettingsCard(scoped)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ── Workspace Files Configuration Store ── */
|
|
142
|
+
export interface WorkspaceFilesConfig {
|
|
143
|
+
maxDepth: number
|
|
144
|
+
showHidden: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const CONFIG_STORAGE_KEY = 'dsh_workspace_files_config'
|
|
148
|
+
|
|
149
|
+
export function loadWorkspaceFilesConfig(): WorkspaceFilesConfig {
|
|
150
|
+
try {
|
|
151
|
+
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(CONFIG_STORAGE_KEY) : null
|
|
152
|
+
if (raw) {
|
|
153
|
+
const parsed = JSON.parse(raw)
|
|
154
|
+
return {
|
|
155
|
+
maxDepth: typeof parsed.maxDepth === 'number' && parsed.maxDepth > 0 ? parsed.maxDepth : 16,
|
|
156
|
+
showHidden: Boolean(parsed.showHidden),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
return { maxDepth: 16, showHidden: false }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function saveWorkspaceFilesConfig(cfg: WorkspaceFilesConfig): void {
|
|
164
|
+
try {
|
|
165
|
+
if (typeof localStorage !== 'undefined') {
|
|
166
|
+
localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(cfg))
|
|
167
|
+
}
|
|
168
|
+
if (typeof window !== 'undefined') {
|
|
169
|
+
window.dispatchEvent(new CustomEvent('dsh-workspace-files:config-change', { detail: cfg }))
|
|
170
|
+
}
|
|
171
|
+
fetch('/dsh-workspace-files/api/config.set', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'content-type': 'application/json' },
|
|
174
|
+
body: JSON.stringify(cfg),
|
|
175
|
+
}).catch(() => {})
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ensureSettingsStyle() {
|
|
180
|
+
if (typeof document === 'undefined') return
|
|
181
|
+
const id = 'dsh-workspace-files-settings-style'
|
|
182
|
+
if (document.getElementById(id)) return
|
|
183
|
+
const style = document.createElement('style')
|
|
184
|
+
style.id = id
|
|
185
|
+
style.textContent = `
|
|
186
|
+
.dsh-wf-card {
|
|
187
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
188
|
+
background: var(--dsw-alias-bg-layer-3, #fff);
|
|
189
|
+
border-radius: 12px;
|
|
190
|
+
list-style: none;
|
|
191
|
+
transition: border-color .16s, background .16s;
|
|
192
|
+
box-sizing: border-box;
|
|
193
|
+
}
|
|
194
|
+
.dsh-wf-card-open {
|
|
195
|
+
background: var(--dsw-alias-bg-layer-2, #f7f8fa);
|
|
196
|
+
border-color: var(--dsw-alias-label-dimmed, #c8ccd4);
|
|
197
|
+
}
|
|
198
|
+
.dsh-wf-header {
|
|
199
|
+
-webkit-appearance: none;
|
|
200
|
+
appearance: none;
|
|
201
|
+
width: 100%;
|
|
202
|
+
font: inherit;
|
|
203
|
+
color: inherit;
|
|
204
|
+
text-align: left;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
background: 0 0;
|
|
207
|
+
border: 0;
|
|
208
|
+
border-radius: 12px;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 12px;
|
|
211
|
+
padding: 14px 16px;
|
|
212
|
+
display: flex;
|
|
213
|
+
}
|
|
214
|
+
.dsh-wf-head-text {
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
flex: 1;
|
|
217
|
+
gap: 4px;
|
|
218
|
+
min-width: 0;
|
|
219
|
+
display: flex;
|
|
220
|
+
}
|
|
221
|
+
.dsh-wf-name {
|
|
222
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
223
|
+
font-size: 15px;
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
line-height: 1.4;
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
gap: 8px;
|
|
229
|
+
}
|
|
230
|
+
.dsh-wf-version {
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
font-weight: 400;
|
|
233
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
234
|
+
}
|
|
235
|
+
.dsh-wf-desc {
|
|
236
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
237
|
+
font-size: 13px;
|
|
238
|
+
line-height: 1.5;
|
|
239
|
+
}
|
|
240
|
+
.dsh-wf-chevron {
|
|
241
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
242
|
+
flex: none;
|
|
243
|
+
transition: transform .16s ease-in-out;
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
}
|
|
246
|
+
.dsh-wf-chevron-open {
|
|
247
|
+
transform: rotate(180deg);
|
|
248
|
+
}
|
|
249
|
+
.dsh-wf-body {
|
|
250
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
251
|
+
margin: 0 16px;
|
|
252
|
+
padding: 8px 0 12px;
|
|
253
|
+
}
|
|
254
|
+
.dsh-wf-row {
|
|
255
|
+
align-items: center;
|
|
256
|
+
gap: 16px;
|
|
257
|
+
padding: 12px 0;
|
|
258
|
+
display: flex;
|
|
259
|
+
justify-content: space-between;
|
|
260
|
+
}
|
|
261
|
+
.dsh-wf-row + .dsh-wf-row {
|
|
262
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(118, 137, 166, 0.12));
|
|
263
|
+
}
|
|
264
|
+
.dsh-wf-label-box {
|
|
265
|
+
flex-direction: column;
|
|
266
|
+
flex: 1;
|
|
267
|
+
gap: 3px;
|
|
268
|
+
min-width: 0;
|
|
269
|
+
display: flex;
|
|
270
|
+
}
|
|
271
|
+
.dsh-wf-label {
|
|
272
|
+
font-size: 13.5px;
|
|
273
|
+
font-weight: 550;
|
|
274
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
275
|
+
}
|
|
276
|
+
.dsh-wf-hint {
|
|
277
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
278
|
+
font-size: 12px;
|
|
279
|
+
line-height: 18px;
|
|
280
|
+
}
|
|
281
|
+
.dsh-wf-ctrl {
|
|
282
|
+
flex-shrink: 0;
|
|
283
|
+
display: flex;
|
|
284
|
+
align-items: center;
|
|
285
|
+
gap: 8px;
|
|
286
|
+
}
|
|
287
|
+
.dsh-wf-seg {
|
|
288
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
289
|
+
border-radius: 8px;
|
|
290
|
+
gap: 2px;
|
|
291
|
+
padding: 2px;
|
|
292
|
+
display: inline-flex;
|
|
293
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.05));
|
|
294
|
+
}
|
|
295
|
+
.dsh-wf-seg-btn {
|
|
296
|
+
font: inherit;
|
|
297
|
+
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
background: 0 0;
|
|
300
|
+
border: none;
|
|
301
|
+
border-radius: 6px;
|
|
302
|
+
padding: 4px 10px;
|
|
303
|
+
font-size: 12px;
|
|
304
|
+
line-height: 18px;
|
|
305
|
+
transition: all 0.15s;
|
|
306
|
+
}
|
|
307
|
+
.dsh-wf-seg-btn:hover {
|
|
308
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
309
|
+
}
|
|
310
|
+
.dsh-wf-seg-active {
|
|
311
|
+
background: var(--dsw-alias-bg-layer-3, #eef0f4);
|
|
312
|
+
color: var(--dsw-alias-brand-primary, #4f6ef7);
|
|
313
|
+
font-weight: 600;
|
|
314
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
315
|
+
}
|
|
316
|
+
.dsh-wf-input {
|
|
317
|
+
box-sizing: border-box;
|
|
318
|
+
border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
|
|
319
|
+
background: var(--dsw-alias-bg-layer-3, #fff);
|
|
320
|
+
width: 64px;
|
|
321
|
+
text-align: center;
|
|
322
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
323
|
+
font: inherit;
|
|
324
|
+
border-radius: 7px;
|
|
325
|
+
padding: 4px 6px;
|
|
326
|
+
font-size: 12.5px;
|
|
327
|
+
line-height: 18px;
|
|
328
|
+
}
|
|
329
|
+
.dsh-wf-input:focus {
|
|
330
|
+
border-color: var(--dsw-alias-brand-primary, #4f6ef7);
|
|
331
|
+
outline: 2px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #4f6ef7) 18%, transparent);
|
|
332
|
+
}
|
|
333
|
+
.dsh-wf-toggle {
|
|
334
|
+
position: relative;
|
|
335
|
+
display: inline-block;
|
|
336
|
+
width: 40px;
|
|
337
|
+
height: 22px;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
}
|
|
340
|
+
.dsh-wf-toggle input {
|
|
341
|
+
opacity: 0;
|
|
342
|
+
width: 0;
|
|
343
|
+
height: 0;
|
|
344
|
+
position: absolute;
|
|
345
|
+
}
|
|
346
|
+
.dsh-wf-toggle-track {
|
|
347
|
+
position: absolute;
|
|
348
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
349
|
+
background-color: var(--dsw-alias-border-l2, #ccc);
|
|
350
|
+
transition: .2s cubic-bezier(.4, 0, .2, 1);
|
|
351
|
+
border-radius: 22px;
|
|
352
|
+
}
|
|
353
|
+
.dsh-wf-toggle input:checked + .dsh-wf-toggle-track {
|
|
354
|
+
background-color: var(--dsw-alias-brand-primary, #4f6ef7);
|
|
355
|
+
}
|
|
356
|
+
.dsh-wf-toggle-thumb {
|
|
357
|
+
position: absolute;
|
|
358
|
+
height: 16px;
|
|
359
|
+
width: 16px;
|
|
360
|
+
left: 3px;
|
|
361
|
+
bottom: 3px;
|
|
362
|
+
background-color: white;
|
|
363
|
+
transition: .2s cubic-bezier(.4, 0, .2, 1);
|
|
364
|
+
border-radius: 50%;
|
|
365
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
366
|
+
}
|
|
367
|
+
.dsh-wf-toggle input:checked + .dsh-wf-toggle-track .dsh-wf-toggle-thumb {
|
|
368
|
+
transform: translateX(18px);
|
|
369
|
+
}
|
|
370
|
+
.dsh-wf-footer {
|
|
371
|
+
margin-top: 8px;
|
|
372
|
+
padding-top: 10px;
|
|
373
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(118, 137, 166, 0.12));
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: space-between;
|
|
377
|
+
}
|
|
378
|
+
.dsh-wf-saved-hint {
|
|
379
|
+
font-size: 11.5px;
|
|
380
|
+
color: var(--dsw-alias-state-success-primary, #10b981);
|
|
381
|
+
display: flex;
|
|
382
|
+
align-items: center;
|
|
383
|
+
gap: 4px;
|
|
384
|
+
}
|
|
385
|
+
.dsh-wf-btn-reset {
|
|
386
|
+
background: transparent;
|
|
387
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
388
|
+
border-radius: 6px;
|
|
389
|
+
padding: 3px 10px;
|
|
390
|
+
font-size: 12px;
|
|
391
|
+
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
392
|
+
cursor: pointer;
|
|
393
|
+
transition: all 0.15s;
|
|
394
|
+
}
|
|
395
|
+
.dsh-wf-btn-reset:hover {
|
|
396
|
+
background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.08));
|
|
397
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
398
|
+
}
|
|
399
|
+
`
|
|
400
|
+
document.head.appendChild(style)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function WorkspaceFilesSettingsCard(): JSX.Element {
|
|
404
|
+
const [open, setOpen] = useState(false)
|
|
405
|
+
const [config, setConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
|
|
406
|
+
const [savedTick, setSavedTick] = useState(false)
|
|
407
|
+
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
ensureSettingsStyle()
|
|
410
|
+
}, [])
|
|
411
|
+
|
|
412
|
+
const updateConfig = (patch: Partial<WorkspaceFilesConfig>) => {
|
|
413
|
+
const next = { ...config, ...patch }
|
|
414
|
+
setConfig(next)
|
|
415
|
+
saveWorkspaceFilesConfig(next)
|
|
416
|
+
setSavedTick(true)
|
|
417
|
+
setTimeout(() => setSavedTick(false), 2000)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const handleDepthSelect = (depth: number) => {
|
|
421
|
+
updateConfig({ maxDepth: depth })
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const handleDepthInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
425
|
+
const val = parseInt(e.target.value, 10)
|
|
426
|
+
if (!isNaN(val) && val >= 1 && val <= 100) {
|
|
427
|
+
updateConfig({ maxDepth: val })
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const handleToggleHidden = () => {
|
|
432
|
+
updateConfig({ showHidden: !config.showHidden })
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const handleReset = () => {
|
|
436
|
+
const def = { maxDepth: 16, showHidden: false }
|
|
437
|
+
setConfig(def)
|
|
438
|
+
saveWorkspaceFilesConfig(def)
|
|
439
|
+
setSavedTick(true)
|
|
440
|
+
setTimeout(() => setSavedTick(false), 2000)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<div className={open ? 'dsh-wf-card dsh-wf-card-open' : 'dsh-wf-card'}>
|
|
445
|
+
<button
|
|
446
|
+
type="button"
|
|
447
|
+
className="dsh-wf-header"
|
|
448
|
+
aria-expanded={open}
|
|
449
|
+
onClick={() => setOpen(!open)}
|
|
450
|
+
>
|
|
451
|
+
<div className="dsh-wf-head-text">
|
|
452
|
+
<div className="dsh-wf-name">
|
|
453
|
+
<span>工作区文件</span>
|
|
454
|
+
<span className="dsh-wf-version">v0.3.0</span>
|
|
455
|
+
</div>
|
|
456
|
+
<div className="dsh-wf-desc">
|
|
457
|
+
浏览与检索工作区文件树,支持递归目录扫描与一键引用。
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
<span className={open ? 'dsh-wf-chevron dsh-wf-chevron-open' : 'dsh-wf-chevron'}>
|
|
461
|
+
<IconChevronDown size={14} />
|
|
462
|
+
</span>
|
|
463
|
+
</button>
|
|
464
|
+
|
|
465
|
+
{open && (
|
|
466
|
+
<div className="dsh-wf-body">
|
|
467
|
+
<div className="dsh-wf-row">
|
|
468
|
+
<div className="dsh-wf-label-box">
|
|
469
|
+
<div className="dsh-wf-label">目录扫描最大深度</div>
|
|
470
|
+
<div className="dsh-wf-hint">
|
|
471
|
+
工作区递归扫描的最大目录层级(默认 16,范围 1~100)。若工程目录较深可适当调大。
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
<div className="dsh-wf-ctrl">
|
|
475
|
+
<div className="dsh-wf-seg">
|
|
476
|
+
{[8, 16, 32, 64].map((d) => (
|
|
477
|
+
<button
|
|
478
|
+
key={d}
|
|
479
|
+
type="button"
|
|
480
|
+
className={config.maxDepth === d ? 'dsh-wf-seg-btn dsh-wf-seg-active' : 'dsh-wf-seg-btn'}
|
|
481
|
+
onClick={() => handleDepthSelect(d)}
|
|
482
|
+
>
|
|
483
|
+
{d}
|
|
484
|
+
</button>
|
|
485
|
+
))}
|
|
486
|
+
</div>
|
|
487
|
+
<input
|
|
488
|
+
type="number"
|
|
489
|
+
min="1"
|
|
490
|
+
max="100"
|
|
491
|
+
className="dsh-wf-input"
|
|
492
|
+
value={config.maxDepth}
|
|
493
|
+
onChange={handleDepthInput}
|
|
494
|
+
title="自定义深度 (1-100)"
|
|
495
|
+
/>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
<div className="dsh-wf-row">
|
|
500
|
+
<div className="dsh-wf-label-box">
|
|
501
|
+
<div className="dsh-wf-label">默认显示隐藏文件</div>
|
|
502
|
+
<div className="dsh-wf-hint">
|
|
503
|
+
打开文件树时是否默认展示以点开头的隐藏文件或文件夹(如 .gitignore, .env 等)。
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
<div className="dsh-wf-ctrl">
|
|
507
|
+
<label className="dsh-wf-toggle">
|
|
508
|
+
<input
|
|
509
|
+
type="checkbox"
|
|
510
|
+
checked={config.showHidden}
|
|
511
|
+
onChange={handleToggleHidden}
|
|
512
|
+
/>
|
|
513
|
+
<span className="dsh-wf-toggle-track">
|
|
514
|
+
<span className="dsh-wf-toggle-thumb" />
|
|
515
|
+
</span>
|
|
516
|
+
</label>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<div className="dsh-wf-footer">
|
|
521
|
+
<div>
|
|
522
|
+
{savedTick ? (
|
|
523
|
+
<span className="dsh-wf-saved-hint">
|
|
524
|
+
<IconCheck size={12} />
|
|
525
|
+
<span>已保存设置</span>
|
|
526
|
+
</span>
|
|
527
|
+
) : (
|
|
528
|
+
<span style={{ fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #8b93a1)' }}>
|
|
529
|
+
设置改动即时生效并同步至工作区文件树
|
|
530
|
+
</span>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
<button
|
|
534
|
+
type="button"
|
|
535
|
+
className="dsh-wf-btn-reset"
|
|
536
|
+
onClick={handleReset}
|
|
537
|
+
>
|
|
538
|
+
恢复默认
|
|
539
|
+
</button>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
)
|
|
114
545
|
}
|
|
115
546
|
|
|
116
547
|
/* ── Main Workspace Files View ── */
|
|
@@ -129,7 +560,8 @@ function WorkspaceFilesView({
|
|
|
129
560
|
const [query, setQuery] = useState('')
|
|
130
561
|
const [toast, setToast] = useState<string | null>(null)
|
|
131
562
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set())
|
|
132
|
-
const [
|
|
563
|
+
const [activeConfig, setActiveConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
|
|
564
|
+
const [showHidden, setShowHidden] = useState(() => loadWorkspaceFilesConfig().showHidden)
|
|
133
565
|
|
|
134
566
|
const toastTimer = useRef<number | null>(null)
|
|
135
567
|
|
|
@@ -139,11 +571,27 @@ function WorkspaceFilesView({
|
|
|
139
571
|
toastTimer.current = window.setTimeout(() => setToast(null), 2000)
|
|
140
572
|
}, [])
|
|
141
573
|
|
|
574
|
+
useEffect(() => {
|
|
575
|
+
const handleCfgChange = (e: any) => {
|
|
576
|
+
if (e.detail) {
|
|
577
|
+
setActiveConfig(e.detail)
|
|
578
|
+
setShowHidden(Boolean(e.detail.showHidden))
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
window.addEventListener('dsh-workspace-files:config-change', handleCfgChange)
|
|
582
|
+
return () => window.removeEventListener('dsh-workspace-files:config-change', handleCfgChange)
|
|
583
|
+
}, [])
|
|
584
|
+
|
|
142
585
|
const loadTree = useCallback(async (quiet = false) => {
|
|
143
586
|
if (!quiet) setLoading(true)
|
|
144
587
|
setError(null)
|
|
145
588
|
try {
|
|
146
|
-
const res = await call<WorkspaceFilesResult>('files.tree', {
|
|
589
|
+
const res = await call<WorkspaceFilesResult>('files.tree', {
|
|
590
|
+
sessionId,
|
|
591
|
+
cwd,
|
|
592
|
+
showHidden,
|
|
593
|
+
maxDepth: activeConfig.maxDepth,
|
|
594
|
+
})
|
|
147
595
|
setData(res)
|
|
148
596
|
|
|
149
597
|
// 默认折叠,并从 localStorage 恢复已记录的展开状态
|
|
@@ -159,7 +607,7 @@ function WorkspaceFilesView({
|
|
|
159
607
|
} finally {
|
|
160
608
|
if (!quiet) setLoading(false)
|
|
161
609
|
}
|
|
162
|
-
}, [sessionId, cwd, showHidden])
|
|
610
|
+
}, [sessionId, cwd, showHidden, activeConfig.maxDepth])
|
|
163
611
|
|
|
164
612
|
useEffect(() => {
|
|
165
613
|
void loadTree()
|
package/src/files.ts
CHANGED
|
@@ -39,7 +39,7 @@ export async function scanWorkspaceTree(
|
|
|
39
39
|
options: { maxDepth?: number; showHidden?: boolean; showIgnored?: boolean } = {},
|
|
40
40
|
): Promise<WorkspaceFilesResult> {
|
|
41
41
|
const root = resolve(cwd)
|
|
42
|
-
const maxDepth = options.maxDepth ??
|
|
42
|
+
const maxDepth = options.maxDepth ?? 16
|
|
43
43
|
const showHidden = options.showHidden ?? false
|
|
44
44
|
const showIgnored = options.showIgnored ?? false
|
|
45
45
|
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
2
3
|
import { scanWorkspaceTree } from './files.js'
|
|
3
4
|
|
|
4
5
|
export const name = '@civilization/dsh-workspace-files'
|
|
5
6
|
export const inject = ['webServer']
|
|
6
7
|
|
|
8
|
+
export interface Config {
|
|
9
|
+
maxDepth?: number
|
|
10
|
+
showHidden?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Config: Schema<Config> = Schema.object({
|
|
14
|
+
maxDepth: Schema.natural().min(1).max(100).default(16).description('工作区文件树扫描的最大文件夹深度(层级)'),
|
|
15
|
+
showHidden: Schema.boolean().default(false).description('默认是否显示以点开头的隐藏文件'),
|
|
16
|
+
}).description('工作区文件:浏览与检索工作区文件树')
|
|
17
|
+
|
|
7
18
|
async function readJsonBody(req: any): Promise<Record<string, any>> {
|
|
8
19
|
if (req.body && typeof req.body === 'object') {
|
|
9
20
|
return req.body
|
|
@@ -79,7 +90,34 @@ function resolveCwd(ctx: Context, body: Record<string, any>): string {
|
|
|
79
90
|
return process.cwd()
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
export
|
|
93
|
+
export const SETTINGS_NS = 'workspace-files'
|
|
94
|
+
|
|
95
|
+
export function apply(ctx: Context, config: Config = {}): void {
|
|
96
|
+
const liveConfig = {
|
|
97
|
+
maxDepth: typeof config?.maxDepth === 'number' && config.maxDepth > 0 ? config.maxDepth : 16,
|
|
98
|
+
showHidden: Boolean(config?.showHidden),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 关键:向 Host 的 settings 服务注册 settings namespace,使设置面板的前端控制器能够枚举到此插件卡片
|
|
102
|
+
if ((ctx as any).inject) {
|
|
103
|
+
(ctx as any).inject(['settings'], (scopedCtx: any) => {
|
|
104
|
+
try {
|
|
105
|
+
const scope = scopedCtx.settings?.register(SETTINGS_NS, Config, {
|
|
106
|
+
base: liveConfig,
|
|
107
|
+
})
|
|
108
|
+
const applyChange = () => {
|
|
109
|
+
const current = scope?.get?.()
|
|
110
|
+
if (typeof current?.maxDepth === 'number') liveConfig.maxDepth = current.maxDepth
|
|
111
|
+
if (typeof current?.showHidden === 'boolean') liveConfig.showHidden = current.showHidden
|
|
112
|
+
}
|
|
113
|
+
applyChange()
|
|
114
|
+
scope?.watch?.(applyChange)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.warn('[dsh-workspace-files] failed to register settings scope:', err)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
83
121
|
const webServer = (ctx as any).webServer
|
|
84
122
|
if (!webServer) return
|
|
85
123
|
|
|
@@ -92,6 +130,23 @@ export function apply(ctx: Context): void {
|
|
|
92
130
|
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname
|
|
93
131
|
const method = pathname.startsWith(prefix) ? pathname.slice(prefix.length) : undefined
|
|
94
132
|
|
|
133
|
+
if (method === 'config.get') {
|
|
134
|
+
writeJson(res, 200, { ok: true, value: liveConfig })
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (method === 'config.set') {
|
|
139
|
+
try {
|
|
140
|
+
const body = await readJsonBody(req)
|
|
141
|
+
if (typeof body.maxDepth === 'number' && body.maxDepth > 0) liveConfig.maxDepth = body.maxDepth
|
|
142
|
+
if (typeof body.showHidden === 'boolean') liveConfig.showHidden = body.showHidden
|
|
143
|
+
writeJson(res, 200, { ok: true, value: liveConfig })
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
writeJson(res, 500, { ok: false, error: { message: err?.message || String(err) } })
|
|
146
|
+
}
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
95
150
|
if (method !== 'files.tree') {
|
|
96
151
|
writeJson(res, 404, { ok: false, error: { message: `Unknown method: ${method}` } })
|
|
97
152
|
return
|
|
@@ -101,9 +156,9 @@ export function apply(ctx: Context): void {
|
|
|
101
156
|
const body = await readJsonBody(req)
|
|
102
157
|
const cwd = resolveCwd(ctx, body)
|
|
103
158
|
const result = await scanWorkspaceTree(cwd, {
|
|
104
|
-
showHidden:
|
|
159
|
+
showHidden: typeof body.showHidden === 'boolean' ? body.showHidden : liveConfig.showHidden,
|
|
105
160
|
showIgnored: Boolean(body.showIgnored),
|
|
106
|
-
maxDepth: typeof body.maxDepth === 'number' ? body.maxDepth :
|
|
161
|
+
maxDepth: typeof body.maxDepth === 'number' && body.maxDepth > 0 ? body.maxDepth : liveConfig.maxDepth,
|
|
107
162
|
})
|
|
108
163
|
writeJson(res, 200, { ok: true, value: result })
|
|
109
164
|
} catch (err: any) {
|