@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.
@@ -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
- }