@fonlan/dsh-quick-commands 0.1.5 → 0.1.6
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/lib/client.js +46 -41
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.tsx +15 -14
- package/src/client/locales.ts +7 -7
- package/src/client/quick-commands.css +1 -191
- package/src/client/settings-section.css +163 -0
- package/src/client/settings-section.tsx +195 -0
- package/src/client/settings-card.tsx +0 -209
package/src/client/index.tsx
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* @fonlan/dsh-quick-commands client half.
|
|
3
3
|
*
|
|
4
4
|
* Registers:
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - "settings.section" page (id "quick-commands", order 330) — per-workspace
|
|
6
|
+
* command CRUD + popup anchor preference, rendered as its own page in the
|
|
7
|
+
* settings sidebar.
|
|
8
|
+
* - "conversation.session.header.utilities" entry (id "quick-commands",
|
|
8
9
|
* order -1 → left of the Session log pill) — the quick-run ▶ button, command
|
|
9
10
|
* menu, and live output popup.
|
|
10
11
|
*/
|
|
@@ -12,9 +13,8 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
12
13
|
type ClientContext = Context
|
|
13
14
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
14
15
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
15
|
-
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
16
16
|
import { QuickCommandsHeaderAction } from './header'
|
|
17
|
-
import {
|
|
17
|
+
import { QuickCommandsSettingsSection } from './settings-section'
|
|
18
18
|
import { LOCALE_NS, zh, en } from './locales'
|
|
19
19
|
import './quick-commands.css'
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ import './quick-commands.css'
|
|
|
22
22
|
interface Slots {
|
|
23
23
|
inject(name: string, callback: () => unknown): unknown
|
|
24
24
|
register(
|
|
25
|
-
def: { name: string;
|
|
25
|
+
def: { name: string; id?: string; order?: number; label?: string | (() => string); locale?: string },
|
|
26
26
|
component: unknown,
|
|
27
27
|
): unknown
|
|
28
28
|
}
|
|
@@ -36,16 +36,17 @@ export function apply(ctx: ClientContext): void {
|
|
|
36
36
|
const t = ctx.locale.bind(LOCALE_NS)
|
|
37
37
|
const slots = (ctx as unknown as { slots: Slots }).slots
|
|
38
38
|
|
|
39
|
-
// The plugin's own
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
slots.inject('settings.plugin.item', () =>
|
|
39
|
+
// The plugin's own settings page (设置 → 侧栏「快捷命令」) rides the
|
|
40
|
+
// "quick-commands" locale namespace: a "settings.section" list entry gives it
|
|
41
|
+
// one row in the settings sidebar and renders the page in the content column.
|
|
42
|
+
slots.inject('settings.section', () =>
|
|
44
43
|
slots.register({
|
|
45
|
-
name: 'settings.
|
|
46
|
-
|
|
44
|
+
name: 'settings.section',
|
|
45
|
+
id: 'quick-commands',
|
|
46
|
+
order: 330,
|
|
47
|
+
label: () => t('settingsTitle'),
|
|
47
48
|
locale: LOCALE_NS,
|
|
48
|
-
},
|
|
49
|
+
}, QuickCommandsSettingsSection as never),
|
|
49
50
|
)
|
|
50
51
|
|
|
51
52
|
// Session-header util: order -1 renders LEFT of the Session log pill
|
package/src/client/locales.ts
CHANGED
|
@@ -37,10 +37,10 @@ export const zh = {
|
|
|
37
37
|
runLost: '运行记录已失效',
|
|
38
38
|
runNotFound: '工作区或命令未找到',
|
|
39
39
|
|
|
40
|
-
// settings
|
|
40
|
+
// settings page (设置 → 侧栏「快捷命令」)
|
|
41
|
+
settingsTitle: '快捷命令',
|
|
41
42
|
cardTitle: '快速命令',
|
|
42
|
-
|
|
43
|
-
collapseOrExpand: '展开 / 收起',
|
|
43
|
+
sectionSub: '为每个工作区配置常用命令,会话头部一键运行',
|
|
44
44
|
cardIntro: '为每个工作区配置常用命令,运行于工作区目录。支持占位符:{workspace} 工作区路径,{cwd} 会话目录,{title} 工作区标题。',
|
|
45
45
|
cardWorkspaces: '工作区',
|
|
46
46
|
cardNoWorkspaces: '没有工作区。请在侧边栏先添加工作区。',
|
|
@@ -54,9 +54,9 @@ export const zh = {
|
|
|
54
54
|
cardAnchor: '输出弹窗位置',
|
|
55
55
|
cardAnchorCorner: '右下角',
|
|
56
56
|
cardAnchorButton: '锚定按钮下方',
|
|
57
|
-
}
|
|
57
|
+
} as const
|
|
58
58
|
|
|
59
|
-
export const en = {
|
|
59
|
+
export const en: Record<keyof typeof zh, string> = {
|
|
60
60
|
buttonTitle: 'Quick commands',
|
|
61
61
|
buttonTooltip: 'Quick commands (current workspace)',
|
|
62
62
|
noWorkspace: 'This session is not in any configured workspace',
|
|
@@ -87,9 +87,9 @@ export const en = {
|
|
|
87
87
|
runLost: 'Run record expired',
|
|
88
88
|
runNotFound: 'Workspace or command not found',
|
|
89
89
|
|
|
90
|
+
settingsTitle: 'Quick Commands',
|
|
90
91
|
cardTitle: 'Quick commands',
|
|
91
|
-
|
|
92
|
-
collapseOrExpand: 'Expand / collapse',
|
|
92
|
+
sectionSub: 'Configure common commands per workspace, run them from the session header',
|
|
93
93
|
cardIntro: 'Configure common commands per workspace; they run in the workspace directory. Placeholders: {workspace} workspace path, {cwd} session cwd, {title} workspace title.',
|
|
94
94
|
cardWorkspaces: 'Workspaces',
|
|
95
95
|
cardNoWorkspaces: 'No workspaces. Add one in the sidebar first.',
|
|
@@ -370,196 +370,6 @@
|
|
|
370
370
|
opacity: 1;
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
/* ── settings card (设置 → 插件配置) — mirrors official PluginCard disclosure ── */
|
|
374
|
-
.qc-settings-card {
|
|
375
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
376
|
-
background: var(--dsw-alias-bg-layer-2, #1f1f21);
|
|
377
|
-
border-radius: 12px;
|
|
378
|
-
list-style: none;
|
|
379
|
-
transition: border-color .16s, background .16s;
|
|
380
|
-
}
|
|
381
|
-
.qc-settings-card:hover {
|
|
382
|
-
border-color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
383
|
-
}
|
|
384
|
-
.qc-settings-card-open {
|
|
385
|
-
background: var(--dsw-alias-bg-layer-1, #27272a);
|
|
386
|
-
border-color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
387
|
-
}
|
|
388
|
-
.qc-settings-header {
|
|
389
|
-
appearance: none;
|
|
390
|
-
width: 100%;
|
|
391
|
-
font: inherit;
|
|
392
|
-
color: inherit;
|
|
393
|
-
text-align: left;
|
|
394
|
-
cursor: pointer;
|
|
395
|
-
background: transparent;
|
|
396
|
-
border: 0;
|
|
397
|
-
border-radius: 12px;
|
|
398
|
-
align-items: center;
|
|
399
|
-
gap: 12px;
|
|
400
|
-
padding: 14px 16px;
|
|
401
|
-
display: flex;
|
|
402
|
-
}
|
|
403
|
-
.qc-settings-header:focus-visible {
|
|
404
|
-
outline: 2px solid var(--dsw-alias-brand-primary, #4a9eff);
|
|
405
|
-
outline-offset: -2px;
|
|
406
|
-
}
|
|
407
|
-
.qc-settings-head-text {
|
|
408
|
-
flex-direction: column;
|
|
409
|
-
flex: 1;
|
|
410
|
-
gap: 4px;
|
|
411
|
-
min-width: 0;
|
|
412
|
-
display: flex;
|
|
413
|
-
}
|
|
414
|
-
.qc-settings-name {
|
|
415
|
-
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
416
|
-
font-size: 15px;
|
|
417
|
-
font-weight: 600;
|
|
418
|
-
line-height: 1.4;
|
|
419
|
-
}
|
|
420
|
-
.qc-settings-description {
|
|
421
|
-
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
422
|
-
font-size: 13px;
|
|
423
|
-
line-height: 1.5;
|
|
424
|
-
}
|
|
425
|
-
.qc-settings-chevron {
|
|
426
|
-
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
427
|
-
flex: none;
|
|
428
|
-
transition: transform .16s;
|
|
429
|
-
}
|
|
430
|
-
.qc-settings-chevron-open {
|
|
431
|
-
transform: rotate(180deg);
|
|
432
|
-
}
|
|
433
|
-
.qc-settings-body {
|
|
434
|
-
border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
435
|
-
margin: 0 16px;
|
|
436
|
-
padding-bottom: 12px;
|
|
437
|
-
display: flex;
|
|
438
|
-
flex-direction: column;
|
|
439
|
-
gap: 10px;
|
|
440
|
-
}
|
|
441
|
-
.qc-settings-intro {
|
|
442
|
-
font-size: 12px;
|
|
443
|
-
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
444
|
-
margin: 8px 0 0;
|
|
445
|
-
line-height: 1.5;
|
|
446
|
-
}
|
|
447
|
-
.qc-settings-anchor {
|
|
448
|
-
display: flex;
|
|
449
|
-
align-items: center;
|
|
450
|
-
gap: 12px;
|
|
451
|
-
font-size: 12px;
|
|
452
|
-
}
|
|
453
|
-
.qc-settings-anchor-label {
|
|
454
|
-
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
455
|
-
font-weight: 500;
|
|
456
|
-
}
|
|
457
|
-
.qc-settings-radio {
|
|
458
|
-
display: inline-flex;
|
|
459
|
-
align-items: center;
|
|
460
|
-
gap: 4px;
|
|
461
|
-
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
462
|
-
cursor: pointer;
|
|
463
|
-
}
|
|
464
|
-
.qc-settings-error {
|
|
465
|
-
font-size: 12px;
|
|
466
|
-
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
467
|
-
}
|
|
468
|
-
.qc-settings-empty {
|
|
469
|
-
font-size: 12px;
|
|
470
|
-
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
471
|
-
margin: 0;
|
|
472
|
-
}
|
|
473
|
-
.qc-settings-workspace {
|
|
474
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
475
|
-
border-radius: 10px;
|
|
476
|
-
padding: 10px;
|
|
477
|
-
display: flex;
|
|
478
|
-
flex-direction: column;
|
|
479
|
-
gap: 8px;
|
|
480
|
-
}
|
|
481
|
-
.qc-settings-workspace-head {
|
|
482
|
-
display: flex;
|
|
483
|
-
align-items: baseline;
|
|
484
|
-
gap: 8px;
|
|
485
|
-
}
|
|
486
|
-
.qc-settings-workspace-title {
|
|
487
|
-
font-size: 13px;
|
|
488
|
-
font-weight: 600;
|
|
489
|
-
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
490
|
-
}
|
|
491
|
-
.qc-settings-workspace-path {
|
|
492
|
-
font-size: 11px;
|
|
493
|
-
font-family: var(--dsw-font-family-mono, monospace);
|
|
494
|
-
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
495
|
-
overflow: hidden;
|
|
496
|
-
text-overflow: ellipsis;
|
|
497
|
-
white-space: nowrap;
|
|
498
|
-
}
|
|
499
|
-
.qc-settings-no-commands {
|
|
500
|
-
font-size: 12px;
|
|
501
|
-
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
502
|
-
margin: 0;
|
|
503
|
-
}
|
|
504
|
-
.qc-settings-command {
|
|
505
|
-
display: flex;
|
|
506
|
-
gap: 6px;
|
|
507
|
-
align-items: center;
|
|
508
|
-
}
|
|
509
|
-
.qc-settings-field {
|
|
510
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
511
|
-
border-radius: 8px;
|
|
512
|
-
background: var(--dsw-alias-bg-layer-1, #2a2a2d);
|
|
513
|
-
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
514
|
-
font-size: 12px;
|
|
515
|
-
padding: 6px 8px;
|
|
516
|
-
outline: none;
|
|
517
|
-
}
|
|
518
|
-
.qc-settings-field::placeholder {
|
|
519
|
-
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
520
|
-
}
|
|
521
|
-
.qc-settings-field:focus {
|
|
522
|
-
border-color: var(--dsw-alias-brand-primary, #4a9eff);
|
|
523
|
-
}
|
|
524
|
-
.qc-settings-field-name {
|
|
525
|
-
width: 160px;
|
|
526
|
-
flex: none;
|
|
527
|
-
}
|
|
528
|
-
.qc-settings-field-command {
|
|
529
|
-
flex: 1;
|
|
530
|
-
font-family: var(--dsw-font-family-mono, monospace);
|
|
531
|
-
}
|
|
532
|
-
.qc-settings-icon-btn {
|
|
533
|
-
border: 0;
|
|
534
|
-
background: transparent;
|
|
535
|
-
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
536
|
-
cursor: pointer;
|
|
537
|
-
padding: 4px;
|
|
538
|
-
display: inline-flex;
|
|
539
|
-
align-items: center;
|
|
540
|
-
border-radius: 6px;
|
|
541
|
-
}
|
|
542
|
-
.qc-settings-icon-btn:hover {
|
|
543
|
-
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
544
|
-
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
545
|
-
}
|
|
546
|
-
.qc-settings-add-btn {
|
|
547
|
-
align-self: flex-start;
|
|
548
|
-
display: inline-flex;
|
|
549
|
-
align-items: center;
|
|
550
|
-
gap: 4px;
|
|
551
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
552
|
-
border-radius: 8px;
|
|
553
|
-
background: transparent;
|
|
554
|
-
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
555
|
-
font-size: 12px;
|
|
556
|
-
padding: 4px 10px;
|
|
557
|
-
cursor: pointer;
|
|
558
|
-
}
|
|
559
|
-
.qc-settings-add-btn:hover {
|
|
560
|
-
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
373
|
/* ── mobile fallback (dsh-mobile-nav) ──────────────────────────────────────
|
|
564
374
|
The dsh-mobile-nav plugin (≤1023px) hides the ENTIRE header utilities seat
|
|
565
375
|
with `[data-mobile-nav="frame"] [data-phase] header > :first-child >
|
|
@@ -605,4 +415,4 @@
|
|
|
605
415
|
min-height: 44px;
|
|
606
416
|
justify-content: center;
|
|
607
417
|
}
|
|
608
|
-
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles for the quick-commands settings page.
|
|
3
|
+
*
|
|
4
|
+
* The page occupies the settings panel content column: a static header
|
|
5
|
+
* (title plus a one-line summary) over the always-visible command editor.
|
|
6
|
+
* Inner controls follow the built-in settings language.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* page layout */
|
|
10
|
+
|
|
11
|
+
.qc-page {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: 16px;
|
|
15
|
+
max-width: 720px;
|
|
16
|
+
}
|
|
17
|
+
.qc-page-head {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 6px;
|
|
21
|
+
}
|
|
22
|
+
.qc-page-title {
|
|
23
|
+
margin: 0;
|
|
24
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
25
|
+
font-size: 15px;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
line-height: 1.4;
|
|
28
|
+
}
|
|
29
|
+
.qc-page-sub {
|
|
30
|
+
margin: 0;
|
|
31
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
line-height: 1.5;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* body: workspace / command-set editor */
|
|
37
|
+
|
|
38
|
+
.qc-settings-body {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: 10px;
|
|
42
|
+
}
|
|
43
|
+
.qc-settings-intro {
|
|
44
|
+
font-size: 12px;
|
|
45
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
46
|
+
margin: 0;
|
|
47
|
+
line-height: 1.5;
|
|
48
|
+
}
|
|
49
|
+
.qc-settings-anchor {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 12px;
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
.qc-settings-anchor-label {
|
|
56
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
}
|
|
59
|
+
.qc-settings-radio {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 4px;
|
|
63
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
.qc-settings-error {
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
69
|
+
}
|
|
70
|
+
.qc-settings-empty {
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
73
|
+
margin: 0;
|
|
74
|
+
}
|
|
75
|
+
.qc-settings-workspace {
|
|
76
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
77
|
+
border-radius: 10px;
|
|
78
|
+
padding: 10px;
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
}
|
|
83
|
+
.qc-settings-workspace-head {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: baseline;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
}
|
|
88
|
+
.qc-settings-workspace-title {
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
92
|
+
}
|
|
93
|
+
.qc-settings-workspace-path {
|
|
94
|
+
font-size: 11px;
|
|
95
|
+
font-family: var(--dsw-font-family-mono, monospace);
|
|
96
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
text-overflow: ellipsis;
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
101
|
+
.qc-settings-no-commands {
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
104
|
+
margin: 0;
|
|
105
|
+
}
|
|
106
|
+
.qc-settings-command {
|
|
107
|
+
display: flex;
|
|
108
|
+
gap: 6px;
|
|
109
|
+
align-items: center;
|
|
110
|
+
}
|
|
111
|
+
.qc-settings-field {
|
|
112
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
background: var(--dsw-alias-bg-layer-1, #2a2a2d);
|
|
115
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
116
|
+
font-size: 12px;
|
|
117
|
+
padding: 6px 8px;
|
|
118
|
+
outline: none;
|
|
119
|
+
}
|
|
120
|
+
.qc-settings-field::placeholder {
|
|
121
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
122
|
+
}
|
|
123
|
+
.qc-settings-field:focus {
|
|
124
|
+
border-color: var(--dsw-alias-brand-primary, #4a9eff);
|
|
125
|
+
}
|
|
126
|
+
.qc-settings-field-name {
|
|
127
|
+
width: 160px;
|
|
128
|
+
flex: none;
|
|
129
|
+
}
|
|
130
|
+
.qc-settings-field-command {
|
|
131
|
+
flex: 1;
|
|
132
|
+
font-family: var(--dsw-font-family-mono, monospace);
|
|
133
|
+
}
|
|
134
|
+
.qc-settings-icon-btn {
|
|
135
|
+
border: 0;
|
|
136
|
+
background: transparent;
|
|
137
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
padding: 4px;
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
}
|
|
144
|
+
.qc-settings-icon-btn:hover {
|
|
145
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
146
|
+
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
147
|
+
}
|
|
148
|
+
.qc-settings-add-btn {
|
|
149
|
+
align-self: flex-start;
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: 4px;
|
|
153
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
154
|
+
border-radius: 8px;
|
|
155
|
+
background: transparent;
|
|
156
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
157
|
+
font-size: 12px;
|
|
158
|
+
padding: 4px 10px;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
.qc-settings-add-btn:hover {
|
|
162
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
163
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick-commands settings page (设置 → 侧栏「快捷命令」).
|
|
3
|
+
*
|
|
4
|
+
* Registers into the "settings.section" list slot, so the page owns one entry
|
|
5
|
+
* in the settings sidebar and renders in the settings panel's content column.
|
|
6
|
+
* The header is static (title + one-line summary) over the always-visible
|
|
7
|
+
* body: the workspace / command-set editor and the popup anchor preference.
|
|
8
|
+
* Body edits instant-save per change; partial rows (empty name/command)
|
|
9
|
+
* persist while the user types.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useRef, useState } from 'react'
|
|
12
|
+
import {
|
|
13
|
+
IconPlusOutline16,
|
|
14
|
+
IconTrashOutline16,
|
|
15
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
16
|
+
import { quickApi } from './api'
|
|
17
|
+
import type { QuickRosterEntry, QuickCommandsSettings } from '../shared/contract'
|
|
18
|
+
import './settings-section.css'
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
t: (key: string) => string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RosterState {
|
|
25
|
+
loaded: boolean
|
|
26
|
+
entries: QuickRosterEntry[]
|
|
27
|
+
anchor: QuickCommandsSettings['popupAnchor']
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type CommandDraft = { name: string; command: string }
|
|
31
|
+
|
|
32
|
+
export function QuickCommandsSettingsSection({ t }: Props): JSX.Element {
|
|
33
|
+
const [state, setState] = useState<RosterState>({ loaded: false, entries: [], anchor: 'corner' })
|
|
34
|
+
const [error, setError] = useState<string | null>(null)
|
|
35
|
+
// Guard concurrent saves: the last write wins via a monotonically bumped token.
|
|
36
|
+
const saveToken = useRef(0)
|
|
37
|
+
|
|
38
|
+
const load = async (): Promise<void> => {
|
|
39
|
+
try {
|
|
40
|
+
const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()])
|
|
41
|
+
setState({ loaded: true, entries, anchor: settings.popupAnchor })
|
|
42
|
+
setError(null)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
setState((s) => ({ ...s, loaded: true }))
|
|
45
|
+
setError(e instanceof Error ? e.message : String(e))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
void load()
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
/** Instant-save the workspace's whole command set; optimistic UI. */
|
|
54
|
+
const saveCommands = async (workspaceId: string, commands: CommandDraft[]): Promise<void> => {
|
|
55
|
+
const token = ++saveToken.current
|
|
56
|
+
try {
|
|
57
|
+
const next = await quickApi.settingsSetCommands(workspaceId, commands)
|
|
58
|
+
if (token !== saveToken.current) return
|
|
59
|
+
setState((s) => ({
|
|
60
|
+
...s,
|
|
61
|
+
anchor: next.popupAnchor,
|
|
62
|
+
entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),
|
|
63
|
+
}))
|
|
64
|
+
setError(null)
|
|
65
|
+
} catch (e) {
|
|
66
|
+
setError(e instanceof Error ? e.message : String(e))
|
|
67
|
+
// Re-sync to server truth on failure.
|
|
68
|
+
void load()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compute the next command list for a workspace, THEN save + update state. */
|
|
73
|
+
const mutateCommands = (workspaceId: string, next: (current: CommandDraft[]) => CommandDraft[]): void => {
|
|
74
|
+
const entry = state.entries.find((e) => e.workspaceId === workspaceId)
|
|
75
|
+
if (entry === undefined) return
|
|
76
|
+
const commands = next(entry.commands)
|
|
77
|
+
// Optimistic update first (row stays visible immediately), then persist.
|
|
78
|
+
setState((s) => ({
|
|
79
|
+
...s,
|
|
80
|
+
entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),
|
|
81
|
+
}))
|
|
82
|
+
void saveCommands(workspaceId, commands)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const patchEntry = (workspaceId: string, index: number, field: 'name' | 'command', value: string): void => {
|
|
86
|
+
mutateCommands(workspaceId, (current) =>
|
|
87
|
+
current.map((c, i) => (i === index ? { ...c, [field]: value } : c)),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const addCommand = (workspaceId: string): void => {
|
|
92
|
+
mutateCommands(workspaceId, (current) => [...current, { name: '', command: '' }])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const removeCommand = (workspaceId: string, index: number): void => {
|
|
96
|
+
mutateCommands(workspaceId, (current) => current.filter((_, i) => i !== index))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const setAnchor = async (anchor: QuickCommandsSettings['popupAnchor']): Promise<void> => {
|
|
100
|
+
setState((s) => ({ ...s, anchor }))
|
|
101
|
+
try {
|
|
102
|
+
const next = await quickApi.settingsSetAnchor(anchor)
|
|
103
|
+
setState((s) => ({ ...s, anchor: next.popupAnchor }))
|
|
104
|
+
} catch (e) {
|
|
105
|
+
setError(e instanceof Error ? e.message : String(e))
|
|
106
|
+
void load()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const title = t('cardTitle')
|
|
111
|
+
const description = t('sectionSub')
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="qc-page">
|
|
115
|
+
<header className="qc-page-head">
|
|
116
|
+
<h3 className="qc-page-title">{title}</h3>
|
|
117
|
+
<p className="qc-page-sub">{description}</p>
|
|
118
|
+
</header>
|
|
119
|
+
<div className="qc-settings-body">
|
|
120
|
+
<p className="qc-settings-intro">{t('cardIntro')}</p>
|
|
121
|
+
|
|
122
|
+
<div className="qc-settings-anchor">
|
|
123
|
+
<span className="qc-settings-anchor-label">{t('cardAnchor')}</span>
|
|
124
|
+
<label className="qc-settings-radio">
|
|
125
|
+
<input
|
|
126
|
+
type="radio"
|
|
127
|
+
name="qc-anchor"
|
|
128
|
+
checked={state.anchor === 'corner'}
|
|
129
|
+
onChange={() => void setAnchor('corner')}
|
|
130
|
+
/>
|
|
131
|
+
{t('cardAnchorCorner')}
|
|
132
|
+
</label>
|
|
133
|
+
<label className="qc-settings-radio">
|
|
134
|
+
<input
|
|
135
|
+
type="radio"
|
|
136
|
+
name="qc-anchor"
|
|
137
|
+
checked={state.anchor === 'button'}
|
|
138
|
+
onChange={() => void setAnchor('button')}
|
|
139
|
+
/>
|
|
140
|
+
{t('cardAnchorButton')}
|
|
141
|
+
</label>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{error !== null && <div className="qc-settings-error">{t('cardSaveError')}: {error}</div>}
|
|
145
|
+
|
|
146
|
+
{state.loaded && state.entries.length === 0 && (
|
|
147
|
+
<p className="qc-settings-empty">{t('cardNoWorkspaces')}</p>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{state.entries.map((entry) => (
|
|
151
|
+
<section key={entry.workspaceId} className="qc-settings-workspace">
|
|
152
|
+
<header className="qc-settings-workspace-head">
|
|
153
|
+
<span className="qc-settings-workspace-title">{entry.title || entry.path || entry.workspaceId}</span>
|
|
154
|
+
<span className="qc-settings-workspace-path">{entry.path}</span>
|
|
155
|
+
</header>
|
|
156
|
+
{entry.commands.length === 0 && (
|
|
157
|
+
<p className="qc-settings-no-commands">{t('cardNoCommands')}</p>
|
|
158
|
+
)}
|
|
159
|
+
{entry.commands.map((command, index) => (
|
|
160
|
+
<div key={`${entry.workspaceId}-${index}`} className="qc-settings-command">
|
|
161
|
+
<input
|
|
162
|
+
className="qc-settings-field qc-settings-field-name"
|
|
163
|
+
placeholder={t('cardName')}
|
|
164
|
+
value={command.name}
|
|
165
|
+
aria-label={t('cardName')}
|
|
166
|
+
onChange={(e) => patchEntry(entry.workspaceId, index, 'name', e.target.value)}
|
|
167
|
+
/>
|
|
168
|
+
<input
|
|
169
|
+
className="qc-settings-field qc-settings-field-command"
|
|
170
|
+
placeholder={t('cardCommand')}
|
|
171
|
+
value={command.command}
|
|
172
|
+
aria-label={t('cardCommand')}
|
|
173
|
+
onChange={(e) => patchEntry(entry.workspaceId, index, 'command', e.target.value)}
|
|
174
|
+
/>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
className="qc-settings-icon-btn"
|
|
178
|
+
aria-label={t('cardRemove')}
|
|
179
|
+
title={t('cardRemove')}
|
|
180
|
+
onClick={() => removeCommand(entry.workspaceId, index)}
|
|
181
|
+
>
|
|
182
|
+
<IconTrashOutline16 size={14} />
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
<button type="button" className="qc-settings-add-btn" onClick={() => addCommand(entry.workspaceId)}>
|
|
187
|
+
<IconPlusOutline16 size={14} />
|
|
188
|
+
{t('cardAddCommand')}
|
|
189
|
+
</button>
|
|
190
|
+
</section>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|