dsh-vscode-mode 0.14.4 → 0.14.5

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.
@@ -18,6 +18,9 @@ import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom,
18
18
  import { isMarkdownPath } from '../markdownPreview.js'
19
19
  import { setActiveEditorPath } from '../activePathStore.js'
20
20
  import { MarkdownPanel } from '../md/mdPanel.js'
21
+ import { isPlanPath, planDocument } from '../planState.js'
22
+ import { MarkdownText, MarkdownDelegateProvider } from '@deepseek-ai/dsh-client-ui-primitives'
23
+ import { isComponentType } from '../md/componentType.js'
21
24
  import { base64ToBytes, bytesToBase64, isPdfPath } from '../pdfPreview.js'
22
25
  import { inNativePath, tryNativeOpen } from '../nativeOpenStore.js'
23
26
  import { clearPendingNav, putPendingNav, readPendingNav } from '../pendingNav.js'
@@ -126,6 +129,8 @@ function syncTypeSuggest(ed, monaco) {
126
129
 
127
130
  /** 日志弹窗每次加载条数(「加载更多」按此步长递增;上限由 host 的 SVN_LOG_SHOW_ALL_CAP 约束)。 */
128
131
  const SVN_LOG_PAGE = 100
132
+ /** Stable Markdown chrome for virtual plans; content stays in session-local memory. */
133
+ const PLAN_MD_LABELS = { code: { copyLabel: '复制', copiedLabel: '已复制' }, footnotes: '脚注' }
129
134
 
130
135
  /**
131
136
  * 日志弹窗状态 → 取数选项(P1-4/P1-5/P1-6:soc/mrg/range 与状态层键保持同源)。
@@ -238,7 +243,15 @@ export function EditorView(props) {
238
243
  const [monacoErr, setMonacoErr] = React.useState(null)
239
244
  const [records, setRecords] = React.useState({})
240
245
  const [tabs, setTabs] = React.useState([])
241
- const [active, setActive] = React.useState(null)
246
+ const [active, setDiskActive] = React.useState(null)
247
+ const [planTabs, setPlanTabs] = React.useState([])
248
+ const [planActive, setPlanActive] = React.useState(null)
249
+ const planActiveRef = React.useRef(null)
250
+ planActiveRef.current = planActive
251
+ const planHostRef = React.useRef(null)
252
+ React.useEffect(() => { setPlanTabs([]); setPlanActive(null) }, [sessionId, cwd])
253
+ /** @private @author ddj 2026年10月08号 @param path Disk tab selection. Leave read-only plan mode before any disk navigation. */
254
+ function setActive(path) { setPlanActive(null); setDiskActive(path) }
242
255
  const [dirtyMap, setDirtyMap] = React.useState({})
243
256
  const [content, setContent] = React.useState(null)
244
257
  const [contentPath, setContentPath] = React.useState(null)
@@ -512,9 +525,13 @@ export function EditorView(props) {
512
525
  lineRegionMapRef.current = lineRegionMap
513
526
  const sum = React.useMemo(() => summarize(Object.values(records)), [records])
514
527
 
515
- const addTab = (path, select) => {
528
+ /** @private @author ddj 2026年10月08号 @param path Disk path. @param select Select tab. @param preview Optional explicit Markdown mode. */
529
+ function addTab(path, select, preview) {
516
530
  setTabs((prev) => insertTab(prev, path))
517
- if (select) setActive(path)
531
+ if (select) {
532
+ if (typeof preview === 'boolean' || isPlanPath(path, cwd, sessionId)) setMdPreview(path, typeof preview === 'boolean' ? preview : true)
533
+ setActive(path)
534
+ }
518
535
  }
519
536
 
520
537
  /**
@@ -536,11 +553,11 @@ export function EditorView(props) {
536
553
  * @param select 是否设为活动页签
537
554
  * @returns 归一后的路径(空值返回 null)
538
555
  */
539
- const addTabNorm = (path, select) => {
556
+ const addTabNorm = (path, select, preview) => {
540
557
  if (!path) return null
541
558
  const normalized = tabPath(path)
542
559
  if (!normalized) return null
543
- addTab(normalized, select)
560
+ addTab(normalized, select, preview)
544
561
  return normalized
545
562
  }
546
563
 
@@ -1238,28 +1255,32 @@ export function EditorView(props) {
1238
1255
  return () => window.removeEventListener('edrv:snippets-changed', onChanged)
1239
1256
  }, [])
1240
1257
 
1241
- React.useEffect(() => {
1242
- const onOpen = (e) => {
1243
- const p = e?.detail?.path
1244
- if (!p) return
1245
- // G9:入口统一归一为工作区相对路径(差异栏/对话链接/LSP 跳转等都经此事件)
1246
- const normalized = tabPath(p)
1247
- if (!normalized) return
1248
- if (e?.detail?.focusDiff === true) {
1249
- recordNav()
1250
- pendingFocusRef.current = { path: normalized, region: null }
1251
- setFocusRequest((value) => value + 1)
1252
- addTab(normalized, true)
1253
- return
1254
- }
1255
- if (e?.detail?.line != null) {
1256
- // LSP/搜索跳转:打开并定位到行列(endLine/endColumn 为目标区间,供落地高亮)
1257
- openFileAt(normalized, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn)
1258
- return
1259
- }
1258
+ const fileOpenRef = React.useRef(null)
1259
+ /** @private @author ddj 2026年10月08号 @param e Routed file request. Always uses the current workspace and save/preview callbacks. */
1260
+ fileOpenRef.current = (e) => {
1261
+ const p = e?.detail?.path
1262
+ if (!p) return
1263
+ // G9:入口统一归一为工作区相对路径(差异栏/对话链接/LSP 跳转等都经此事件)
1264
+ const normalized = tabPath(p)
1265
+ if (!normalized) return
1266
+ if (e?.detail?.focusDiff === true) {
1260
1267
  recordNav()
1261
- addTab(normalized, true)
1268
+ pendingFocusRef.current = { path: normalized, region: null }
1269
+ setFocusRequest((value) => value + 1)
1270
+ addTab(normalized, true, false)
1271
+ return
1262
1272
  }
1273
+ if (e?.detail?.line != null) {
1274
+ // LSP/搜索跳转:打开并定位到行列(endLine/endColumn 为目标区间,供落地高亮)
1275
+ openFileAt(normalized, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn)
1276
+ return
1277
+ }
1278
+ recordNav()
1279
+ addTab(normalized, true, e?.detail?.preview)
1280
+ }
1281
+ React.useEffect(() => {
1282
+ /** @private @author ddj 2026年10月08号 @param event File navigation event. Dispatch to the latest render closure. */
1283
+ const onOpen = (event) => fileOpenRef.current?.(event)
1263
1284
  const onShowLauncher = (event) => {
1264
1285
  const tab = event?.detail?.tab
1265
1286
  if (tab === 'pending' || tab === 'archive') setLauncherTab(tab)
@@ -1807,7 +1828,11 @@ export function EditorView(props) {
1807
1828
  React.useEffect(() => {
1808
1829
  if (!monaco) return
1809
1830
  registerThemes(monaco)
1810
- const apply = () => applyOfficial(monaco)
1831
+ /** @private @author ddj 2026年10月09号 同步 Monaco 主题与行内删除代码着色。 */
1832
+ const apply = () => {
1833
+ const themeId = applyOfficial(monaco)
1834
+ diffRendererRef.current.refresh(themeId)
1835
+ }
1811
1836
  apply()
1812
1837
  window.addEventListener('edrv:theme-change', apply)
1813
1838
  const query = typeof window.matchMedia === 'function' ? window.matchMedia('(prefers-color-scheme: dark)') : null
@@ -2619,13 +2644,19 @@ export function EditorView(props) {
2619
2644
  * @author ddj 2026年09月18号
2620
2645
  * @param path Markdown 文件路径
2621
2646
  */
2647
+ /** @private @author ddj 2026年10月08号 @param path Markdown disk path. @param preview Desired mode; explicit line opens always select source. */
2648
+ function setMdPreview(path, preview) {
2649
+ if (!isMarkdownPath(path) || mdPreviewRef.current.has(path) === preview) return
2650
+ toggleMdPreview(path)
2651
+ }
2652
+ /** @private @author ddj 2026年10月08号 @param path Markdown file. Toggle preview after flushing the active file's save and view state. */
2622
2653
  const toggleMdPreview = (path) => {
2623
2654
  if (!path) return
2624
2655
  const next = new Set(mdPreviewRef.current)
2625
2656
  const entering = !next.has(path)
2626
2657
  if (entering) {
2627
2658
  flushSave()
2628
- saveViewState(path)
2659
+ saveViewState(active)
2629
2660
  next.add(path)
2630
2661
  } else {
2631
2662
  next.delete(path)
@@ -3220,6 +3251,7 @@ export function EditorView(props) {
3220
3251
 
3221
3252
  /** 关闭当前活动页签(Ctrl+F4 与菜单动作共用;无页签时提示)。 */
3222
3253
  const closeActiveTab = () => {
3254
+ if (planActiveRef.current) { closePlan(planActiveRef.current); return }
3223
3255
  if (!activeRef.current) { setStatus('无打开的文件'); return }
3224
3256
  closeTab(activeRef.current)
3225
3257
  }
@@ -3346,7 +3378,7 @@ export function EditorView(props) {
3346
3378
  dapTrace('open-file-at', { input: path, normalized: tracePath, line, cwd })
3347
3379
  recordNav()
3348
3380
  // G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
3349
- const normalized = addTabNorm(path, true)
3381
+ const normalized = addTabNorm(path, true, false)
3350
3382
  if (!normalized) return
3351
3383
  stageFileAt(normalized, line, column, endLine, endColumn)
3352
3384
  }
@@ -3366,12 +3398,28 @@ export function EditorView(props) {
3366
3398
  setFocusRequest((value) => value + 1)
3367
3399
  }
3368
3400
 
3369
- /**
3370
- * Open a receipt target without losing edits or leaving the requested source hidden.
3371
- * @private @author ddj 2026年09月28号
3372
- * @param request Claimed external open request.
3373
- */
3401
+ /** @private @author ddj 2026年10月08号 @param plan Complete virtual document. Keeps virtual addresses out of all disk/save/LSP/SVN flows. */
3402
+ function openPlanDoc(plan) {
3403
+ const document = planDocument(plan?.address, plan)
3404
+ if (!document) return
3405
+ flushSave()
3406
+ saveViewState(active)
3407
+ setSvnDiff(null)
3408
+ setDiskActive(null)
3409
+ setPlanTabs((prev) => [...prev.filter((item) => item.address !== document.address), document].slice(-32))
3410
+ setPlanActive(document.address)
3411
+ setError(null)
3412
+ setStatus('计划预览 · 只读')
3413
+ }
3414
+ /** @private @author ddj 2026年10月08号 @param address Virtual tab. @param event Optional close click. Removes only in-memory plan content. */
3415
+ function closePlan(address, event) {
3416
+ event?.stopPropagation()
3417
+ setPlanTabs((prev) => prev.filter((item) => item.address !== address))
3418
+ if (planActiveRef.current === address) setActive(tabsRef.current[0]?.path ?? null)
3419
+ }
3420
+ /** @private @author ddj 2026年10月08号 @param request Claimed disk or virtual open. Flushes saves without replacing dirty models. */
3374
3421
  function openReceiptFile(request) {
3422
+ if (request.plan) { openPlanDoc(request.plan); return }
3375
3423
  if (!request.path) return
3376
3424
  let path = receiptTabPath(request.path, cwd)
3377
3425
  const existing = tabsRef.current.find((tab) => receiptPath(tab.path, cwd) === receiptPath(path, cwd))
@@ -3384,7 +3432,7 @@ export function EditorView(props) {
3384
3432
  if (positioned && mdPreviewRef.current.has(path)) toggleMdPreview(path)
3385
3433
  setError(null)
3386
3434
  if (monacoErr) setMonacoErr(null)
3387
- addTab(path, true)
3435
+ addTab(path, true, positioned ? false : request.preview)
3388
3436
  if (positioned) stageFileAt(path, request.line ?? 1, request.column ?? 1)
3389
3437
  const moved = receiptLoad && receiptPath(receiptLoad.path, receiptLoad.cwd) !== receiptPath(path, cwd)
3390
3438
  if (path !== active || !(loadError || receiptLoad?.status === 'error' || moved)) return
@@ -3423,10 +3471,14 @@ export function EditorView(props) {
3423
3471
 
3424
3472
  /** @private @author ddj 2026年09月28号 @param request Claimed target. @returns Correlated actual readiness. */
3425
3473
  function receiptReady(request) {
3474
+ if (request.plan) return request.sessionId === sessionId && planActive === request.plan.address
3475
+ && planTabs.some((item) => item.address === request.plan.address && item.markdown === request.plan.markdown)
3476
+ && planHostRef.current?.getAttribute('data-plan-address') === request.plan.address && !svnDiff
3426
3477
  return receiptReadyFor(request, receiptView(request))
3427
3478
  }
3428
3479
  /** @private @author ddj 2026年09月28号 @param request Claimed target. @returns Only its current load/render error. */
3429
3480
  function receiptError(request) {
3481
+ if (request.plan) return planDocument(request.plan.address, request.plan) ? null : '计划内容或资源地址无效'
3430
3482
  return receiptErrorFor(request, receiptView(request))
3431
3483
  }
3432
3484
  useOpenReceipt({ sessionId, open: openReceiptFile, ready: receiptReady, error: receiptError })
@@ -3514,11 +3566,20 @@ export function EditorView(props) {
3514
3566
  // @author ddj 2026年09月09号
3515
3567
  (dirtyMap[t.path] ? React.createElement('span', { className: 'edrv-tab-star', title: '未保存修改' }, '*') : null),
3516
3568
  (t.pinned ? null : React.createElement('span', { className: 'edrv-tab-x', title: '关闭', onClick: (e) => { e.stopPropagation(); closeTab(t.path) } }, '×')))),
3569
+ planTabs.map((plan) => React.createElement('div', {
3570
+ key: plan.address, className: 'edrv-tab' + (plan.address === planActive ? ' edrv-tab-active' : ''),
3571
+ title: plan.title + '(计划预览 · 只读)', onClick: () => openPlanDoc(plan),
3572
+ }, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 } }, plan.title),
3573
+ React.createElement('span', { className: 'edrv-tab-x', title: '关闭', onClick: (event) => closePlan(plan.address, event) }, '×'))),
3517
3574
  (openInput
3518
3575
  ? React.createElement('input', { className: 'edrv-path-input', autoFocus: true, placeholder: '输入工作区相对/绝对路径,回车打开', value: pathDraft, onChange: (e) => setPathDraft(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') openPath(); if (e.key === 'Escape') setOpenInput(false) } })
3519
3576
  : React.createElement('button', { className: 'edrv-tab-add', title: '打开文件(输入路径)', onClick: () => setOpenInput(true) }, '+')))
3520
3577
 
3521
- const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
3578
+ const activePlan = planTabs.find((plan) => plan.address === planActive)
3579
+ const pathBar = activePlan ? React.createElement('div', { className: 'edrv-pathbar', title: activePlan.address },
3580
+ React.createElement('span', { className: 'edrv-pb-name' }, activePlan.title),
3581
+ React.createElement('span', { className: 'edrv-pb-meta' }, '计划预览 · 只读 · 审批请使用对话中的官方控件'))
3582
+ : React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
3522
3583
  React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
3523
3584
  React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开文件'),
3524
3585
  (recentUpdate && !svnUpdateDialog && sameUpdateOwner(recentUpdate, sessionId, scope)
@@ -3681,7 +3742,17 @@ export function EditorView(props) {
3681
3742
  retry ? React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: retry }, '重试') : null)
3682
3743
 
3683
3744
  let body
3684
- if (svnDiff) {
3745
+ if (activePlan) {
3746
+ const labels = PLAN_MD_LABELS
3747
+ const text = activePlan.markdown
3748
+ const render = isComponentType(MarkdownText)
3749
+ ? React.createElement(MarkdownDelegateProvider, {
3750
+ openFile: (path, options) => options?.line != null ? openFileAt(path, options.line) : openFile(path, false),
3751
+ }, React.createElement(MarkdownText, { text, streaming: false, labels, variant: 'body' }))
3752
+ : React.createElement('pre', { className: 'edrv-mdview-pre' }, text)
3753
+ body = React.createElement('div', { className: 'edrv-mdview', ref: planHostRef, 'data-plan-address': activePlan.address },
3754
+ React.createElement('div', { className: 'edrv-mdview-stage' }, React.createElement('div', { className: 'edrv-mdview-doc' }, render)))
3755
+ } else if (svnDiff) {
3685
3756
  // 差异视图优先占位(覆盖编辑器主体):只读审阅态,关闭后回到原编辑器
3686
3757
  body = React.createElement(SvnDiffPanel, {
3687
3758
  key: 'edrv-svndiff:' + svnDiff.scheme + ':' + svnDiff.path,
@@ -69,14 +69,14 @@ export function OfficialSideTab(props) {
69
69
  if (fileHit) {
70
70
  const line = navigation?.params?.line
71
71
  window.dispatchEvent(new CustomEvent('edrv:open-editor', {
72
- detail: { path: fileHit.path, line: typeof line === 'number' ? line : undefined },
72
+ detail: { path: fileHit.path, line: typeof line === 'number' ? line : undefined, preview: navigation?.params?.preview },
73
73
  }))
74
74
  return
75
75
  }
76
76
  // 页类型 Tab:params 携带打开请求(line = 行引用/工具行跳转的行号定位)
77
77
  const request = resolveNavOpen(navigation?.params)
78
78
  window.dispatchEvent(new CustomEvent('edrv:open-editor', {
79
- detail: { path: request.path, focusDiff: request.focusDiff, line: resolveNavLine(navigation?.params) },
79
+ detail: { path: request.path, focusDiff: request.focusDiff, line: resolveNavLine(navigation?.params), preview: navigation?.params?.preview },
80
80
  }))
81
81
  })
82
82