dsh-plugin-capabilities 0.3.1 → 0.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-capabilities",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Manage skills and MCP servers from the Web UI Settings. 在设置页管理 dsh 的技能与 MCP 服务器。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -260,6 +260,9 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
260
260
  open={detailRepo !== null || detailServer !== null}
261
261
  onClose={() => setDetail(null)}
262
262
  title={detailRepo !== null ? title(detailRepo) : detailServer !== null ? title(detailServer) : ''}
263
+ closeLabel={t('close')}
264
+ className="dpc-marketDialog"
265
+ contentClassName="dpc-marketContent"
263
266
  footer={
264
267
  <>
265
268
  {detailRepo !== null && (
@@ -296,6 +299,24 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
296
299
  </>
297
300
  }
298
301
  >
302
+ {(detailRepo !== null || detailServer !== null) && (
303
+ <div className="dpc-detailTagsRow">
304
+ {detailRepo !== null && detailRepo.skillCount !== undefined && (
305
+ <span className="dpc-tag">{t('marketSkillCount').replace('{n}', String(detailRepo.skillCount))}</span>
306
+ )}
307
+ {detailRepo !== null && detailRepo.installedId !== null && (
308
+ <span className="dpc-tag" data-kind="source">{t('marketInstalled')}</span>
309
+ )}
310
+ {detailServer !== null && (
311
+ <>
312
+ <span className="dpc-tag">{detailServer.transport}</span>
313
+ {detailServer.runtime !== undefined && <span className="dpc-tag">{detailServer.runtime}</span>}
314
+ {detailServer.category !== undefined && <span className="dpc-tag">{detailServer.category}</span>}
315
+ {detailServer.installed && <span className="dpc-tag" data-kind="source">{t('marketInstalled')}</span>}
316
+ </>
317
+ )}
318
+ </div>
319
+ )}
299
320
  {detailRepo !== null && (
300
321
  <>
301
322
  <p className="dpc-detailDesc">{longText(detailRepo)}</p>
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useEffect, useState } from 'react'
7
7
  import type { ReactElement } from 'react'
8
- import { Button, IconRefreshOutline14, IconSkillOutline16, Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
8
+ import { Button, IconRefreshOutline14, IconSkillOutline16, MarkdownText, Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
9
9
  import { CSS } from './css.ts'
10
10
  import type { OpenTarget, Translate } from './index.ts'
11
11
  import type { RootRowView } from './MarketTab.tsx'
@@ -77,6 +77,8 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
77
77
  const { t, injected } = props
78
78
  const [skills, setSkills] = useState<SkillRowView[] | null>(null)
79
79
  const [editor, setEditor] = useState<EditorState | null>(null)
80
+ /** Content pane of the editor modal: rendered Markdown vs the raw editor. */
81
+ const [preview, setPreview] = useState(false)
80
82
  const [confirmName, setConfirmName] = useState<string | null>(null)
81
83
  const [busy, setBusy] = useState(false)
82
84
  const [outcome, setOutcome] = useState<{ ok: boolean; text: string } | null>(null)
@@ -106,6 +108,7 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
106
108
 
107
109
  const openCreate = (): void => {
108
110
  setFormError(null)
111
+ setPreview(false)
109
112
  setEditor({ mode: 'create', name: '', description: '', whenToUse: '', modelInvocable: true, userInvocable: true, content: '' })
110
113
  }
111
114
 
@@ -114,6 +117,9 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
114
117
  setFormError(null)
115
118
  try {
116
119
  const body = await injected.get(skill.name)
120
+ // Read-only sources open on the rendered preview; editable ones on the
121
+ // plain-text editor. The toggle in the content row flips either way.
122
+ setPreview(!skill.editable)
117
123
  setEditor({
118
124
  mode: skill.editable ? 'edit' : 'view',
119
125
  name: skill.name,
@@ -485,15 +491,32 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
485
491
  {t('userInvocable')}
486
492
  </label>
487
493
  </div>
488
- <label className="dpc-label">
489
- <span>{t('skillContent')}</span>
490
- <textarea
491
- className="dpc-textarea"
492
- value={editor.content}
493
- readOnly={readOnly}
494
- onChange={(event) => setEditor({ ...editor, content: event.target.value })}
495
- />
496
- </label>
494
+ <div className="dpc-label">
495
+ <div className="dpc-cardRow">
496
+ <span>{t('skillContent')}</span>
497
+ <span className="dpc-spacer" />
498
+ <div className="dpc-segments" role="tablist" aria-label={t('skillContent')}>
499
+ <button type="button" role="tab" aria-selected={preview} className="dpc-segment" data-active={preview ? 'true' : undefined} onClick={() => setPreview(true)}>
500
+ {t('skillPreview')}
501
+ </button>
502
+ <button type="button" role="tab" aria-selected={!preview} className="dpc-segment" data-active={!preview ? 'true' : undefined} onClick={() => setPreview(false)}>
503
+ {readOnly ? t('skillPlainText') : t('edit')}
504
+ </button>
505
+ </div>
506
+ </div>
507
+ {preview
508
+ ? (typeof MarkdownText === 'function'
509
+ ? <div className="dpc-mdPreview"><MarkdownText text={editor.content} /></div>
510
+ : <textarea className="dpc-textarea" value={editor.content} readOnly />)
511
+ : (
512
+ <textarea
513
+ className="dpc-textarea"
514
+ value={editor.content}
515
+ readOnly={readOnly}
516
+ onChange={(event) => setEditor({ ...editor, content: event.target.value })}
517
+ />
518
+ )}
519
+ </div>
497
520
  {formError !== null && <p className="dpc-formError">{formError}</p>}
498
521
  <div className="dpc-cardRow">
499
522
  <span className="dpc-spacer" />
package/src/client/css.ts CHANGED
@@ -46,7 +46,7 @@ export const CSS = `
46
46
  .dpc-label{display:flex;flex-direction:column;gap:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
47
47
  .dpc-label>span:first-child{color:var(--dsw-alias-label-tertiary)}
48
48
  .dpc-input,.dpc-textarea,.dpc-select{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;outline:none;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
49
- .dpc-textarea{min-height:240px;resize:vertical;font-family:var(--ds-font-family-code);line-height:1.5}
49
+ .dpc-textarea{min-height:320px;resize:vertical;font-family:var(--ds-font-family-code);line-height:1.5}
50
50
  .dpc-textarea[data-short='true']{min-height:96px}
51
51
  .dpc-input:focus-visible,.dpc-textarea:focus-visible,.dpc-select:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-state-business-primary) 18%,transparent)}
52
52
  .dpc-checks{display:flex;gap:16px;font-size:13px;line-height:20px}
@@ -59,8 +59,9 @@ export const CSS = `
59
59
  /* Editor dialogs (new skill / server): 640px wide so markdown bodies and
60
60
  command/arg/env lines stop wrapping mid-token; the content column scrolls
61
61
  on short viewports instead of clipping past the dialog edge. */
62
- .dpc-modalForm.dpc-modalForm{width:min(640px,100%)}
62
+ .dpc-modalForm.dpc-modalForm{width:min(760px,100%)}
63
63
  .dpc-modalScroll.dpc-modalScroll{max-height:calc(100vh - 160px);overflow-y:auto}
64
+ .dpc-mdPreview{min-height:320px;max-height:60vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-1);font-size:13px}
64
65
  .dpc-importScroll{display:flex;flex-direction:column;gap:14px;max-height:min(400px,52vh);overflow-y:auto;padding:2px 4px 2px 2px}
65
66
  .dpc-importGroup{display:flex;flex-direction:column;gap:8px}
66
67
  .dpc-importHead{display:flex;align-items:center;gap:8px;padding:0 2px}
@@ -108,14 +109,21 @@ export const CSS = `
108
109
  .dpc-format .dpc-form{margin-top:8px}
109
110
  .dpc-formatHint{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
110
111
  .dpc-code{margin:4px 0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:8px 10px;overflow-x:auto;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code);font-size:11px;line-height:17px;white-space:pre}
111
- /* Market detail modal. */
112
+ /* Market detail modal. The host dialog card is a fixed 380px column with no
113
+ scroll, which the long-form intros turn into a clipped strip — widen it
114
+ (doubled selector outranks the module class regardless of style order),
115
+ cap the height, and scroll inside the content region. */
116
+ .dpc-marketDialog.dpc-marketDialog{width:min(720px,100%);max-height:calc(100vh - 48px)}
117
+ .dpc-marketContent{overflow-y:auto;min-height:0}
118
+ .dpc-detailTagsRow{display:flex;flex-wrap:wrap;gap:4px;margin:0 0 8px}
112
119
  .dpc-detailDesc{margin:0;font-size:13px;line-height:20px}
113
- .dpc-detailSection{margin-top:12px;display:flex;flex-direction:column;gap:6px}
120
+ .dpc-detailSection{margin-top:14px;display:flex;flex-direction:column;gap:6px}
114
121
  .dpc-detailLabel{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}
115
122
  .dpc-detailTags{display:flex;flex-wrap:wrap;gap:4px}
116
- .dpc-skillList{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
123
+ .dpc-skillList{margin:0;padding:0;list-style:none;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px 18px}
117
124
  .dpc-skillItem{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:18px;min-width:0}
118
125
  .dpc-skillName{font-family:var(--ds-font-family-code);color:var(--dsw-alias-label-primary);white-space:nowrap;flex:none}
119
126
  .dpc-skillDesc{color:var(--dsw-alias-label-secondary);min-width:0}
127
+ @media(max-width:560px){.dpc-skillList{grid-template-columns:minmax(0,1fr)}}
120
128
  @media(max-width:680px){.dpc-cards{grid-template-columns:minmax(0,1fr)}}
121
129
  `
@@ -14,6 +14,8 @@ export const zh = {
14
14
  skillDescription: '描述',
15
15
  skillWhenToUse: '使用时机(可选)',
16
16
  skillContent: '正文(Markdown)',
17
+ skillPreview: 'Markdown 预览',
18
+ skillPlainText: '纯文本',
17
19
  modelInvocable: '允许模型调用',
18
20
  userInvocable: '允许用户 / 调用',
19
21
  save: '保存',
@@ -145,6 +147,8 @@ export const en = {
145
147
  skillDescription: 'Description',
146
148
  skillWhenToUse: 'When to use (optional)',
147
149
  skillContent: 'Body (Markdown)',
150
+ skillPreview: 'Markdown preview',
151
+ skillPlainText: 'Plain text',
148
152
  modelInvocable: 'Model-invocable',
149
153
  userInvocable: 'User-invocable (/)',
150
154
  save: 'Save',
@@ -40,6 +40,12 @@ declare module '@deepseek-ai/dsh-client-ui-primitives' {
40
40
  className?: string | undefined
41
41
  }): ReactElement
42
42
 
43
+ /** Untrusted-Markdown renderer over the app's own mdast pipeline. */
44
+ export function MarkdownText(props: {
45
+ text: string
46
+ streaming?: boolean | undefined
47
+ }): ReactElement
48
+
43
49
  export function IconRefreshOutline14(props: {
44
50
  className?: string | undefined
45
51
  size?: number | undefined