@fonlan/dsh-quick-commands 0.1.4 → 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 +110 -74
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/header.tsx +89 -34
- package/src/client/index.tsx +15 -14
- package/src/client/locales.ts +7 -7
- package/src/client/quick-commands.css +25 -190
- package/src/client/settings-section.css +163 -0
- package/src/client/settings-section.tsx +195 -0
- package/src/client/settings-card.tsx +0 -209
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Quick-commands settings card (设置 → 插件配置): an expandable plugin card —
|
|
3
|
-
* header (title + description + chevron) collapsed by default, body reveals
|
|
4
|
-
* the workspace × command-set editor and the popup anchor preference. Body
|
|
5
|
-
* edits instant-save per change; partial rows (empty name/command) persist
|
|
6
|
-
* while the user types.
|
|
7
|
-
*
|
|
8
|
-
* Mirrors the official PluginCard disclosure pattern (header button with
|
|
9
|
-
* aria-expanded, rotate-180 chevron, body separated by a top border).
|
|
10
|
-
*/
|
|
11
|
-
import { useEffect, useRef, useState } from 'react'
|
|
12
|
-
import {
|
|
13
|
-
IconChevronDownOutline14,
|
|
14
|
-
IconPlusOutline16,
|
|
15
|
-
IconTrashOutline16,
|
|
16
|
-
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
17
|
-
import { quickApi } from './api'
|
|
18
|
-
import type { QuickRosterEntry, QuickCommandsSettings } from '../shared/contract'
|
|
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 QuickCommandsSettingsCard({ t }: Props): JSX.Element {
|
|
33
|
-
// Card-local disclosure state (a reading gesture, like the official card).
|
|
34
|
-
const [open, setOpen] = useState(false)
|
|
35
|
-
const [state, setState] = useState<RosterState>({ loaded: false, entries: [], anchor: 'corner' })
|
|
36
|
-
const [error, setError] = useState<string | null>(null)
|
|
37
|
-
// Guard concurrent saves: the last write wins via a monotonically bumped token.
|
|
38
|
-
const saveToken = useRef(0)
|
|
39
|
-
|
|
40
|
-
const load = async (): Promise<void> => {
|
|
41
|
-
try {
|
|
42
|
-
const [entries, settings] = await Promise.all([quickApi.workspacesList(), quickApi.settingsGet()])
|
|
43
|
-
setState({ loaded: true, entries, anchor: settings.popupAnchor })
|
|
44
|
-
setError(null)
|
|
45
|
-
} catch (e) {
|
|
46
|
-
setState((s) => ({ ...s, loaded: true }))
|
|
47
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
void load()
|
|
53
|
-
}, [])
|
|
54
|
-
|
|
55
|
-
/** Instant-save the workspace's whole command set; optimistic UI. */
|
|
56
|
-
const saveCommands = async (workspaceId: string, commands: CommandDraft[]): Promise<void> => {
|
|
57
|
-
const token = ++saveToken.current
|
|
58
|
-
try {
|
|
59
|
-
const next = await quickApi.settingsSetCommands(workspaceId, commands)
|
|
60
|
-
if (token !== saveToken.current) return
|
|
61
|
-
setState((s) => ({
|
|
62
|
-
...s,
|
|
63
|
-
anchor: next.popupAnchor,
|
|
64
|
-
entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),
|
|
65
|
-
}))
|
|
66
|
-
setError(null)
|
|
67
|
-
} catch (e) {
|
|
68
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
69
|
-
// Re-sync to server truth on failure.
|
|
70
|
-
void load()
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Compute the next command list for a workspace, THEN save + update state. */
|
|
75
|
-
const mutateCommands = (workspaceId: string, next: (current: CommandDraft[]) => CommandDraft[]): void => {
|
|
76
|
-
const entry = state.entries.find((e) => e.workspaceId === workspaceId)
|
|
77
|
-
if (entry === undefined) return
|
|
78
|
-
const commands = next(entry.commands)
|
|
79
|
-
// Optimistic update first (row stays visible immediately), then persist.
|
|
80
|
-
setState((s) => ({
|
|
81
|
-
...s,
|
|
82
|
-
entries: s.entries.map((e) => (e.workspaceId === workspaceId ? { ...e, commands } : e)),
|
|
83
|
-
}))
|
|
84
|
-
void saveCommands(workspaceId, commands)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const patchEntry = (workspaceId: string, index: number, field: 'name' | 'command', value: string): void => {
|
|
88
|
-
mutateCommands(workspaceId, (current) =>
|
|
89
|
-
current.map((c, i) => (i === index ? { ...c, [field]: value } : c)),
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const addCommand = (workspaceId: string): void => {
|
|
94
|
-
mutateCommands(workspaceId, (current) => [...current, { name: '', command: '' }])
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const removeCommand = (workspaceId: string, index: number): void => {
|
|
98
|
-
mutateCommands(workspaceId, (current) => current.filter((_, i) => i !== index))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const setAnchor = async (anchor: QuickCommandsSettings['popupAnchor']): Promise<void> => {
|
|
102
|
-
setState((s) => ({ ...s, anchor }))
|
|
103
|
-
try {
|
|
104
|
-
const next = await quickApi.settingsSetAnchor(anchor)
|
|
105
|
-
setState((s) => ({ ...s, anchor: next.popupAnchor }))
|
|
106
|
-
} catch (e) {
|
|
107
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
108
|
-
void load()
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const title = t('cardTitle')
|
|
113
|
-
const description = t('cardDescription')
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<li className={'qc-settings-card' + (open ? ' qc-settings-card-open' : '')}>
|
|
117
|
-
<button
|
|
118
|
-
type="button"
|
|
119
|
-
className="qc-settings-header"
|
|
120
|
-
aria-expanded={open}
|
|
121
|
-
aria-label={`${t('collapseOrExpand')}: ${title}`}
|
|
122
|
-
onClick={() => setOpen((v) => !v)}
|
|
123
|
-
>
|
|
124
|
-
<span className="qc-settings-head-text">
|
|
125
|
-
<span className="qc-settings-name">{title}</span>
|
|
126
|
-
<span className="qc-settings-description">{description}</span>
|
|
127
|
-
</span>
|
|
128
|
-
<IconChevronDownOutline14 className={'qc-settings-chevron' + (open ? ' qc-settings-chevron-open' : '')} />
|
|
129
|
-
</button>
|
|
130
|
-
|
|
131
|
-
{open && (
|
|
132
|
-
<div className="qc-settings-body">
|
|
133
|
-
<p className="qc-settings-intro">{t('cardIntro')}</p>
|
|
134
|
-
|
|
135
|
-
<div className="qc-settings-anchor">
|
|
136
|
-
<span className="qc-settings-anchor-label">{t('cardAnchor')}</span>
|
|
137
|
-
<label className="qc-settings-radio">
|
|
138
|
-
<input
|
|
139
|
-
type="radio"
|
|
140
|
-
name="qc-anchor"
|
|
141
|
-
checked={state.anchor === 'corner'}
|
|
142
|
-
onChange={() => void setAnchor('corner')}
|
|
143
|
-
/>
|
|
144
|
-
{t('cardAnchorCorner')}
|
|
145
|
-
</label>
|
|
146
|
-
<label className="qc-settings-radio">
|
|
147
|
-
<input
|
|
148
|
-
type="radio"
|
|
149
|
-
name="qc-anchor"
|
|
150
|
-
checked={state.anchor === 'button'}
|
|
151
|
-
onChange={() => void setAnchor('button')}
|
|
152
|
-
/>
|
|
153
|
-
{t('cardAnchorButton')}
|
|
154
|
-
</label>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
{error !== null && <div className="qc-settings-error">{t('cardSaveError')}: {error}</div>}
|
|
158
|
-
|
|
159
|
-
{state.loaded && state.entries.length === 0 && (
|
|
160
|
-
<p className="qc-settings-empty">{t('cardNoWorkspaces')}</p>
|
|
161
|
-
)}
|
|
162
|
-
|
|
163
|
-
{state.entries.map((entry) => (
|
|
164
|
-
<section key={entry.workspaceId} className="qc-settings-workspace">
|
|
165
|
-
<header className="qc-settings-workspace-head">
|
|
166
|
-
<span className="qc-settings-workspace-title">{entry.title || entry.path || entry.workspaceId}</span>
|
|
167
|
-
<span className="qc-settings-workspace-path">{entry.path}</span>
|
|
168
|
-
</header>
|
|
169
|
-
{entry.commands.length === 0 && (
|
|
170
|
-
<p className="qc-settings-no-commands">{t('cardNoCommands')}</p>
|
|
171
|
-
)}
|
|
172
|
-
{entry.commands.map((command, index) => (
|
|
173
|
-
<div key={`${entry.workspaceId}-${index}`} className="qc-settings-command">
|
|
174
|
-
<input
|
|
175
|
-
className="qc-settings-field qc-settings-field-name"
|
|
176
|
-
placeholder={t('cardName')}
|
|
177
|
-
value={command.name}
|
|
178
|
-
aria-label={t('cardName')}
|
|
179
|
-
onChange={(e) => patchEntry(entry.workspaceId, index, 'name', e.target.value)}
|
|
180
|
-
/>
|
|
181
|
-
<input
|
|
182
|
-
className="qc-settings-field qc-settings-field-command"
|
|
183
|
-
placeholder={t('cardCommand')}
|
|
184
|
-
value={command.command}
|
|
185
|
-
aria-label={t('cardCommand')}
|
|
186
|
-
onChange={(e) => patchEntry(entry.workspaceId, index, 'command', e.target.value)}
|
|
187
|
-
/>
|
|
188
|
-
<button
|
|
189
|
-
type="button"
|
|
190
|
-
className="qc-settings-icon-btn"
|
|
191
|
-
aria-label={t('cardRemove')}
|
|
192
|
-
title={t('cardRemove')}
|
|
193
|
-
onClick={() => removeCommand(entry.workspaceId, index)}
|
|
194
|
-
>
|
|
195
|
-
<IconTrashOutline16 size={14} />
|
|
196
|
-
</button>
|
|
197
|
-
</div>
|
|
198
|
-
))}
|
|
199
|
-
<button type="button" className="qc-settings-add-btn" onClick={() => addCommand(entry.workspaceId)}>
|
|
200
|
-
<IconPlusOutline16 size={14} />
|
|
201
|
-
{t('cardAddCommand')}
|
|
202
|
-
</button>
|
|
203
|
-
</section>
|
|
204
|
-
))}
|
|
205
|
-
</div>
|
|
206
|
-
)}
|
|
207
|
-
</li>
|
|
208
|
-
)
|
|
209
|
-
}
|