dsh-ide-git 0.18.5 → 0.18.7

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.18.5",
3
+ "version": "0.18.7",
4
4
  "main": "./src/index.js",
5
5
  "description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ide-git",
3
- "version": "0.18.5",
3
+ "version": "0.18.7",
4
4
  "description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -7762,6 +7762,55 @@ window.__ModuleLoader__.load({
7762
7762
  return index < 0 ? '' : filePath.slice(0, index)
7763
7763
  }
7764
7764
 
7765
+ /* A REAL directory tree for the "by directory" grouping (v0.18.6): files
7766
+ are inserted by path segments, then a folder that has no direct files
7767
+ and exactly one child is COMPRESSED into that child ("a/b/c" as one
7768
+ node, VS Code source-control style) — the flat per-directory heads the
7769
+ user rejected rendered every folder of a chain side by side, so
7770
+ `pkg/service` and `pkg/service/impl` looked like unrelated siblings.
7771
+ Returns the compressed roots (sorted), every foldable node id (for
7772
+ fold-all), the files sitting at the group root, and the two recursive
7773
+ helpers the renderer needs. */
7774
+ function buildDirForest(items, groupKey) {
7775
+ const root = { dirs: new Map(), files: [], path: [] }
7776
+ for (const item of items) {
7777
+ const dir = dirName(item.path)
7778
+ const parts = dir === '' ? [] : dir.split('/')
7779
+ let node = root
7780
+ for (const part of parts) {
7781
+ let child = node.dirs.get(part)
7782
+ if (child === undefined) {
7783
+ child = { dirs: new Map(), files: [], path: node.path.concat([part]) }
7784
+ node.dirs.set(part, child)
7785
+ }
7786
+ node = child
7787
+ }
7788
+ node.files.push(item)
7789
+ }
7790
+ const compress = (node) => {
7791
+ let merged = node
7792
+ while (merged.files.length === 0 && merged.dirs.size === 1) {
7793
+ const only = merged.dirs.values().next().value
7794
+ merged = { dirs: only.dirs, files: only.files, path: only.path }
7795
+ }
7796
+ return merged
7797
+ }
7798
+ const countOf = (node) => {
7799
+ let total = node.files.length
7800
+ for (const child of node.dirs.values()) total += countOf(child)
7801
+ return total
7802
+ }
7803
+ const sortedChildren = (node) => Array.from(node.dirs.values()).sort((a, b) => a.path[a.path.length - 1].localeCompare(b.path[b.path.length - 1]))
7804
+ const ids = []
7805
+ const collect = (node) => {
7806
+ ids.push(groupKey + '|' + node.path.join('/'))
7807
+ for (const child of sortedChildren(node)) collect(child)
7808
+ }
7809
+ const roots = sortedChildren(root).map((child) => compress(child))
7810
+ for (const node of roots) collect(node)
7811
+ return { roots: roots, rootFiles: root.files, ids: ids, countOf: countOf, sortedChildren: sortedChildren }
7812
+ }
7813
+
7765
7814
  /* ============================== commit graph ============================== */
7766
7815
 
7767
7816
  function buildRows(commits) {
@@ -10155,8 +10204,12 @@ window.__ModuleLoader__.load({
10155
10204
  // stage/discard buttons below would silently do nothing (git needs -f).
10156
10205
  const readOnly = props.group === 'ignored'
10157
10206
  const status = readOnly ? 'I' : (raw === '' ? 'M' : raw.charAt(0))
10207
+ /* Tree depth in dir-grouped mode (-1 = flat): every level under a
10208
+ folder head indents one 12px step past the base 26px. */
10209
+ const depth = typeof props.depth === 'number' ? props.depth : -1
10158
10210
  return E('div', {
10159
- className: 'dig-row dig-row-file' + (props.selected === true ? ' dig-row-selected' : '') + (props.nested === true ? ' dig-row-nested' : ''),
10211
+ className: 'dig-row dig-row-file' + (props.selected === true ? ' dig-row-selected' : '') + (depth >= 0 ? ' dig-row-nested' : ''),
10212
+ style: depth >= 0 ? { paddingLeft: (26 + depth * 12) + 'px' } : undefined,
10160
10213
  title: item.origPath === undefined ? item.path : item.origPath + ' → ' + item.path,
10161
10214
  /* The click is handled ONE level up (ChangesPanel): plain click only
10162
10215
  selects, DOUBLE-click opens the diff (v0.18.3 — one interaction model
@@ -10173,14 +10226,14 @@ window.__ModuleLoader__.load({
10173
10226
  // In dir-grouped mode the folder head already names the directory —
10174
10227
  // repeating the full path at the end of every nested row is what made
10175
10228
  // the tree read as a flat list (v0.18.5). Flat modes keep it.
10176
- props.nested === true ? null : E('span', { className: 'dig-row-sub dig-row-dir' }, item.path.slice(-1) === '/' ? '' : dirName(item.path)),
10229
+ depth >= 0 ? null : E('span', { className: 'dig-row-sub dig-row-dir' }, item.path.slice(-1) === '/' ? '' : dirName(item.path)),
10177
10230
  item.additions > 0 ? E('span', { className: 'dig-stat-add' }, '+' + item.additions) : null,
10178
10231
  item.deletions > 0 ? E('span', { className: 'dig-stat-del' }, '-' + item.deletions) : null,
10179
10232
  readOnly ? null : (props.group === 'staged'
10180
10233
  ? E('button', { type: 'button', className: 'dig-mini', title: t('action.unstage'), onClick: (event) => { event.stopPropagation(); props.onUnstage(item) } }, E(Icon, { name: 'minus', size: 12 }))
10181
10234
  : E('button', { type: 'button', className: 'dig-mini', title: t('action.stage'), onClick: (event) => { event.stopPropagation(); props.onStage(item) } }, E(Icon, { name: 'plus', size: 12 }))),
10182
10235
  readOnly ? null : E('button', { type: 'button', className: 'dig-mini', title: t('action.discard'), onClick: (event) => { event.stopPropagation(); props.onDiscard(item, props.group) } }, E(Icon, { name: 'undo', size: 12 })))
10183
- }, (prev, next) => prev.item === next.item && prev.group === next.group && prev.t === next.t && prev.selected === next.selected && prev.nested === next.nested)
10236
+ }, (prev, next) => prev.item === next.item && prev.group === next.group && prev.t === next.t && prev.selected === next.selected && prev.depth === next.depth)
10184
10237
 
10185
10238
  function ChangesPanel(props) {
10186
10239
  const t = props.t
@@ -10416,18 +10469,14 @@ window.__ModuleLoader__.load({
10416
10469
  setAmend(false)
10417
10470
  return true
10418
10471
  }
10419
- const entryRow = (key, item, nested) => E(ChangeRow, {
10472
+ const entryRow = (key, item, depth) => E(ChangeRow, {
10420
10473
  key: key + ':' + item.path, item: item, group: key, t: t,
10421
- nested: nested === true,
10474
+ depth: typeof depth === 'number' ? depth : -1,
10422
10475
  selected: selection.has(key + ':' + item.path) === true,
10423
10476
  onRowClick: handleRowClick,
10424
10477
  onDblClick: props.onDiff,
10425
10478
  onStage: props.onStage, onUnstage: props.onUnstage, onDiscard: props.onDiscard, onDiff: props.onDiff, onMenu: props.onChangeMenu,
10426
10479
  })
10427
- const folderOf = (item) => {
10428
- const dir = dirName(item.path)
10429
- return dir === '' ? t('changes.rootDir') : dir
10430
- }
10431
10480
  /* The "..." menu's sort orders (v0.15). 'git' keeps git's own status
10432
10481
  order; the others are pure client-side sorts of already-loaded
10433
10482
  entries — no extra git calls, no re-fetch. */
@@ -10455,29 +10504,48 @@ window.__ModuleLoader__.load({
10455
10504
  for (const item of sorted) out.push({ kind: 'row', key: key + ':' + item.path, h: ITEM_HEIGHTS.row, node: entryRow(key, item) })
10456
10505
  return
10457
10506
  }
10458
- const folders = new Map()
10459
- for (const item of sorted) {
10460
- const name = folderOf(item)
10461
- if (folders.has(name) === false) folders.set(name, [])
10462
- folders.get(name).push(item)
10463
- }
10464
- for (const name of Array.from(folders.keys()).sort()) {
10465
- const id = key + '|' + name
10507
+ const forest = buildDirForest(sorted, key)
10508
+ const renderFolder = (node, depth) => {
10509
+ const id = key + '|' + node.path.join('/')
10466
10510
  const open = foldedDirs[id] !== true
10467
- const list = folders.get(name)
10468
10511
  out.push({
10469
10512
  kind: 'folder', key: 'folder:' + id, h: ITEM_HEIGHTS.folder,
10470
10513
  node: E('button', {
10471
10514
  type: 'button', className: 'dig-folder-head',
10515
+ style: { paddingLeft: (4 + depth * 12) + 'px' },
10472
10516
  onClick: () => setFoldedDirs((previous) => Object.assign({}, previous, { [id]: previous[id] !== true })),
10473
10517
  },
10474
10518
  E('span', { className: 'dig-chevron' + (open ? ' dig-chevron-open' : '') }, E(Icon, { name: 'chevron', size: 10 })),
10475
10519
  E('span', { className: 'dig-row-icon dig-tone-accent' }, E(Icon, { name: 'folder', size: 10 })),
10476
- E('span', { className: 'dig-folder-name' }, name),
10477
- E('span', { className: 'dig-count' }, String(list.length))),
10520
+ E('span', { className: 'dig-folder-name' }, node.path.join('/')),
10521
+ E('span', { className: 'dig-count' }, String(forest.countOf(node)))),
10522
+ })
10523
+ if (open === true) {
10524
+ /* VS Code order inside a folder: subfolders first (recursively,
10525
+ each indented one more level), then this folder's own files. */
10526
+ for (const child of forest.sortedChildren(node)) renderFolder(child, depth + 1)
10527
+ for (const item of sortedOf(node.files)) out.push({ kind: 'row', key: key + ':' + item.path, h: ITEM_HEIGHTS.row, node: entryRow(key, item, depth) })
10528
+ }
10529
+ }
10530
+ /* Files sitting at the group ROOT get their own (root) head, sorted
10531
+ first — the flat grouping used to give them a localized head too. */
10532
+ if (forest.rootFiles.length > 0) {
10533
+ const rootId = key + '|/'
10534
+ const rootOpen = foldedDirs[rootId] !== true
10535
+ out.push({
10536
+ kind: 'folder', key: 'folder:' + rootId, h: ITEM_HEIGHTS.folder,
10537
+ node: E('button', {
10538
+ type: 'button', className: 'dig-folder-head',
10539
+ onClick: () => setFoldedDirs((previous) => Object.assign({}, previous, { [rootId]: previous[rootId] !== true })),
10540
+ },
10541
+ E('span', { className: 'dig-chevron' + (rootOpen ? ' dig-chevron-open' : '') }, E(Icon, { name: 'chevron', size: 10 })),
10542
+ E('span', { className: 'dig-row-icon dig-tone-accent' }, E(Icon, { name: 'folder', size: 10 })),
10543
+ E('span', { className: 'dig-folder-name' }, t('changes.rootDir')),
10544
+ E('span', { className: 'dig-count' }, String(forest.rootFiles.length))),
10478
10545
  })
10479
- if (open === true) for (const item of list) out.push({ kind: 'row', key: key + ':' + item.path, h: ITEM_HEIGHTS.row, node: entryRow(key, item, true) })
10546
+ if (rootOpen === true) for (const item of sortedOf(forest.rootFiles)) out.push({ kind: 'row', key: key + ':' + item.path, h: ITEM_HEIGHTS.row, node: entryRow(key, item, 0) })
10480
10547
  }
10548
+ for (const node of forest.roots) renderFolder(node, 0)
10481
10549
  }
10482
10550
  const emitGroup = (key, title, entries) => {
10483
10551
  if (entries.length === 0) return
@@ -11544,6 +11612,10 @@ window.__ModuleLoader__.load({
11544
11612
  ⋯ menu's 展开全部/折叠全部 entries drive the same map the folder
11545
11613
  headers toggle. */
11546
11614
  const [foldedDirs, setFoldedDirs] = useState({})
11615
+ /* Last-seen "HEAD hash|branch" signature, from the poll summaries
11616
+ (v0.18.7). A ref, not state: a change triggers a data reload, not a
11617
+ render. null until the first poll tick records the baseline. */
11618
+ const headRef = useRef(null)
11547
11619
  const refresh = useCallback(async (opts) => {
11548
11620
  if (repoRoot === null) return
11549
11621
  const withFetch = opts !== null && typeof opts === 'object' && opts.fetchRemote === true && remoteReady === true
@@ -11554,6 +11626,12 @@ window.__ModuleLoader__.load({
11554
11626
  try { await request('fetch', Object.assign({}, base, { prune: true })) } catch (error) { void error }
11555
11627
  }
11556
11628
  const [summaryData, branchesData, page] = await Promise.all([loadSummary(), loadBranches(), loadCommits(0)])
11629
+ /* Keep the poll's HEAD signature in step (v0.18.7): after an
11630
+ in-panel commit/checkout the next tick must not re-pull
11631
+ branches + history all over again. */
11632
+ if (summaryData !== null && typeof summaryData === 'object' && typeof summaryData.head === 'string') {
11633
+ headRef.current = summaryData.head + '|' + (summaryData.branch === undefined ? '' : summaryData.branch)
11634
+ }
11557
11635
  /* The write side of the first-paint cache: exactly the FIRST page
11558
11636
  (a paged fetch never reaches this path), replaced on every
11559
11637
  refresh — the auto-refresh included — so a session switch through
@@ -11598,11 +11676,29 @@ window.__ModuleLoader__.load({
11598
11676
  if (inFlight === true) return
11599
11677
  inFlight = true
11600
11678
  void (async () => {
11601
- try { await loadSummary() } catch (error) { void error } finally { inFlight = false }
11679
+ try {
11680
+ const data = await loadSummary()
11681
+ /* v0.18.7: the poll used to be summary-only, so commits and
11682
+ checkouts made OUTSIDE the panel (terminal, another tool)
11683
+ never reached the history and branch lists until a manual
11684
+ refresh. Each summary now carries HEAD + branch; when that
11685
+ signature moves, re-pull branches + history. The panel's own
11686
+ actions already refresh everything through run() → refresh()
11687
+ (which also re-records the signature), so this stays quiet
11688
+ for in-panel work. The null baseline (first tick) only
11689
+ records — the mount refresh already pulled everything. */
11690
+ const sig = data !== null && typeof data === 'object' && typeof data.head === 'string'
11691
+ ? data.head + '|' + (data.branch === undefined ? '' : data.branch)
11692
+ : null
11693
+ if (sig !== null && headRef.current !== null && sig !== headRef.current) {
11694
+ await Promise.all([loadBranches(), loadCommits(0)])
11695
+ }
11696
+ if (sig !== null) headRef.current = sig
11697
+ } catch (error) { void error } finally { inFlight = false }
11602
11698
  })()
11603
11699
  }, AUTO_REFRESH_MS)
11604
11700
  return () => clearInterval(timer)
11605
- }, [props.visible, repoRoot, loadSummary])
11701
+ }, [props.visible, repoRoot, loadSummary, loadBranches, loadCommits])
11606
11702
 
11607
11703
  const run = useCallback(async (method, payload) => {
11608
11704
  const result = await guard(() => request(method, Object.assign({}, base, payload)))
@@ -11616,6 +11712,10 @@ window.__ModuleLoader__.load({
11616
11712
  setSummary(null)
11617
11713
  setBranches(null)
11618
11714
  setCommits([])
11715
+ /* The signature is per-repo: force a fresh baseline for the new one
11716
+ so the first poll tick doesn't mistake the repo switch for an
11717
+ outside HEAD move. */
11718
+ headRef.current = null
11619
11719
  setDetail(null)
11620
11720
  setSelectedHash(null)
11621
11721
  setPatch('')
@@ -12600,10 +12700,12 @@ window.__ModuleLoader__.load({
12600
12700
  if (changes !== null) {
12601
12701
  const groups = [['conflicted', changes.conflicted], ['staged', changes.staged], ['unstaged', changes.unstaged], ['untracked', changes.untracked], ['ignored', changes.ignored === undefined ? [] : changes.ignored]]
12602
12702
  for (const pair of groups) {
12603
- for (const item of pair[1]) {
12604
- const dir = dirName(item.path)
12605
- next[pair[0] + '|' + (dir === '' ? t('changes.rootDir') : dir)] = true
12606
- }
12703
+ /* Fold every node of the compressed tree — the ids come from
12704
+ the same builder the renderer uses, so single-chain nodes
12705
+ ("a/b/c") fold exactly where they render (v0.18.6). */
12706
+ const forest = buildDirForest(pair[1], pair[0])
12707
+ if (forest.rootFiles.length > 0) next[pair[0] + '|/'] = true
12708
+ for (const id of forest.ids) next[id] = true
12607
12709
  }
12608
12710
  }
12609
12711
  setFoldedDirs(next)
@@ -13598,7 +13700,8 @@ window.__ModuleLoader__.load({
13598
13700
  '.dig-star-filled svg path{fill:currentColor}',
13599
13701
  // Rows nested under a dir-group folder head are indented and drop the
13600
13702
  // trailing directory repeat, so the grouping reads as a tree.
13601
- '.dig-row-nested{padding-left:26px}',
13703
+ /* Nested rows indent inline (26px base + 12px per tree depth) — the CSS
13704
+ class only marks them for tests/probes. */
13602
13705
  '.dig-graph{flex:none}',
13603
13706
  '.dig-commit-date{color:var(--dsw-alias-label-tertiary);flex:none;width:54px;font-variant-numeric:tabular-nums;font-weight:500}',
13604
13707
  '.dig-commit-author{color:var(--dsw-alias-label-secondary);flex:none;width:96px;overflow:hidden;text-overflow:ellipsis;font-weight:500}',
package/src/index.js CHANGED
@@ -614,7 +614,7 @@ async function summary(payload) {
614
614
  large `status` scan no longer serialises behind five cheap calls
615
615
  (issue #11: the sum of ~ten sequential commands became the max of two
616
616
  chains). */
617
- const [branchInfo, statusOut, unstagedStat, stagedStat, ignored, stashCount, worktrees, operation] = await Promise.all([
617
+ const [branchInfo, statusOut, unstagedStat, stagedStat, ignored, stashCount, worktrees, operation, head] = await Promise.all([
618
618
  (async () => {
619
619
  const branch = await currentBranchOf(root)
620
620
  const detached = branch === 'HEAD'
@@ -629,6 +629,10 @@ async function summary(payload) {
629
629
  stashCountOf(root),
630
630
  worktreesOf(root),
631
631
  operationOf(root),
632
+ /* HEAD hash (v0.18.7): lets the client's poll notice OUTSIDE commits and
633
+ checkouts — the panel's own actions already refresh everything, but
634
+ nothing else watches the repo. Empty string on an unborn branch. */
635
+ git(root, ['rev-parse', 'HEAD']).then((out) => out.trim()).catch(() => ''),
632
636
  ])
633
637
  const groups = classify(parsePorcelain(statusOut))
634
638
  return {
@@ -639,6 +643,7 @@ async function summary(payload) {
639
643
  upstream: branchInfo.upstream,
640
644
  ahead: branchInfo.tracking.ahead,
641
645
  behind: branchInfo.tracking.behind,
646
+ head: head,
642
647
  changes: {
643
648
  staged: decorate(groups.staged, stagedStat),
644
649
  unstaged: decorate(groups.unstaged, unstagedStat),