dsh-ide-git 0.19.8 → 0.19.10

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.19.8",
3
+ "version": "0.19.10",
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.19.8",
3
+ "version": "0.19.10",
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
@@ -7068,6 +7068,20 @@ window.__ModuleLoader__.load({
7068
7068
  return kind + ':' + entry.name
7069
7069
  }
7070
7070
 
7071
+ /* v0.19.9: the REMOTE section folds by path exactly like the local one. In
7072
+ the common single-remote repo every row starts with the same "<remote>/"
7073
+ prefix, which would nest every tree under one folder named after the
7074
+ remote — noise, not grouping — so the strip applies only when EVERY name
7075
+ shares that first segment AND carries it (a bare "main" beside
7076
+ "origin/main" means mixed shapes and keeps full names). */
7077
+ function remoteTreeStripOf(names) {
7078
+ if (names.length === 0) return ''
7079
+ const first = names.map((name) => String(name).split('/')[0])
7080
+ if (new Set(first).size !== 1) return ''
7081
+ const prefix = first[0] + '/'
7082
+ return names.every((name) => String(name).startsWith(prefix) === true) ? prefix : ''
7083
+ }
7084
+
7071
7085
  /* Join selected values for the clipboard — one per line, the shape a
7072
7086
  terminal or an editor expects when pasting a list. */
7073
7087
  function clipboardLines(values) {
@@ -10249,6 +10263,22 @@ window.__ModuleLoader__.load({
10249
10263
  return props.vertical === true ? rect.width : rect.height
10250
10264
  }
10251
10265
  const sign = props.side === 'next' ? -1 : 1
10266
+ /* Pointermove arrives faster than frames can paint, and every dragPane
10267
+ * does a forced reflow (measurePanes) plus two setStates — handling each
10268
+ * event is layout thrashing, and the report (#16 follow-up) is exactly
10269
+ * that: a drag starts smooth, then feels glued to the cursor. The latest
10270
+ * px parks in a ref and ONE rAF per frame hands it to the clamp; pointerup
10271
+ * flushes the parked value first so a gesture never ends on a stale size. */
10272
+ const frameRef = useRef(0)
10273
+ const pendingRef = useRef(null)
10274
+ const flushRef = useRef(null)
10275
+ useEffect(() => () => { if (frameRef.current !== 0) cancelAnimationFrame(frameRef.current) }, [])
10276
+ flushRef.current = () => {
10277
+ if (frameRef.current !== 0) { cancelAnimationFrame(frameRef.current); frameRef.current = 0 }
10278
+ const value = pendingRef.current
10279
+ pendingRef.current = null
10280
+ if (value !== null) props.onDrag(value)
10281
+ }
10252
10282
  const onPointerDown = (event) => {
10253
10283
  if (event.button !== undefined && event.button !== 0) return
10254
10284
  dragRef.current = {
@@ -10264,11 +10294,13 @@ window.__ModuleLoader__.load({
10264
10294
  const drag = dragRef.current
10265
10295
  if (drag === null || drag.id !== event.pointerId) return
10266
10296
  const at = props.vertical === true ? event.clientX : event.clientY
10267
- props.onDrag(drag.size + sign * (at - drag.at))
10297
+ pendingRef.current = drag.size + sign * (at - drag.at)
10298
+ if (frameRef.current === 0) frameRef.current = requestAnimationFrame(() => flushRef.current())
10268
10299
  }
10269
10300
  const onPointerUp = () => {
10270
10301
  if (dragRef.current === null) return
10271
10302
  dragRef.current = null
10303
+ if (frameRef.current !== 0) flushRef.current()
10272
10304
  props.onEnd()
10273
10305
  }
10274
10306
  const onKeyDown = (event) => {
@@ -10478,6 +10510,13 @@ window.__ModuleLoader__.load({
10478
10510
  double-click browses the current branch's history, right-click opens
10479
10511
  the same row menu (quick jump back to HEAD lives there). */
10480
10512
  const headEntry = locals.find((entry) => entry.head === true) || null
10513
+ /* v0.19.9: remote rows group by their path BELOW the "<remote>/" prefix;
10514
+ the full name still drives every selKey/menu/checkout path. */
10515
+ const remoteStrip = remoteTreeStripOf(remotes.map((entry) => entry.name))
10516
+ const remoteNameOf = (entry) => {
10517
+ const name = String(entry.name)
10518
+ return remoteStrip !== '' && name.startsWith(remoteStrip) === true ? name.slice(remoteStrip.length) : name
10519
+ }
10481
10520
  /* A refresh that makes a picked ref disappear must not keep it selected. */
10482
10521
  useEffect(() => {
10483
10522
  reportGestureRef.current = false
@@ -10507,10 +10546,14 @@ window.__ModuleLoader__.load({
10507
10546
  * row only SELECTS it (checking out rewrites the working tree), so the row
10508
10547
  * carries the selection and a double-click does the checkout.
10509
10548
  */
10510
- const forestOf = (entries) => {
10549
+ /* nameOf (v0.19.9) lets the remote section group by its name WITHOUT the
10550
+ "<remote>/" prefix (see remoteTreeStripOf) while every row keeps its
10551
+ full entry.name for selection, menus and checkout. */
10552
+ const forestOf = (entries, nameOf) => {
10553
+ const labelOf = nameOf === undefined ? (entry) => entry.name : nameOf
10511
10554
  const root = { folders: new Map(), leaves: [] }
10512
10555
  for (const entry of entries) {
10513
- const parts = String(entry.name).split('/')
10556
+ const parts = String(labelOf(entry)).split('/')
10514
10557
  let node = root
10515
10558
  for (let index = 0; index < parts.length - 1; index += 1) {
10516
10559
  const key = parts[index]
@@ -10591,7 +10634,9 @@ window.__ModuleLoader__.load({
10591
10634
  for (const leaf of node.leaves) out.push(branchRow(leaf.entry, leaf.label, depth))
10592
10635
  return out
10593
10636
  }
10594
- const section = (key, title, entries, decorate, grouped) => E('div', { className: 'dig-section', key: key },
10637
+ /* ns (v0.19.9) namespaces the fold ids of a grouped section ("remote/…")
10638
+ so its folders remember their state independently of the local ones. */
10639
+ const section = (key, title, entries, decorate, grouped, ns, nameOf) => E('div', { className: 'dig-section', key: key },
10595
10640
  E('button', {
10596
10641
  type: 'button', className: 'dig-section-head',
10597
10642
  onClick: () => setCollapsed((previous) => Object.assign({}, previous, { [key]: !previous[key] })),
@@ -10602,14 +10647,16 @@ window.__ModuleLoader__.load({
10602
10647
  E('span', null, title)),
10603
10648
  collapsed[key] === true ? null : E('div', { className: 'dig-section-body' },
10604
10649
  grouped === true
10605
- ? rowsOf(forestOf(entries.map((entry) => decorate(entry))), '', 0)
10650
+ ? rowsOf(forestOf(entries.map((entry) => decorate(entry)), nameOf), ns || '', 0)
10606
10651
  : entries.map((entry) => branchRow(decorate(entry), undefined, 0))))
10607
10652
  /* Render-order mirror for Shift-ranges and Ctrl+A, plus the selKey→entry
10608
10653
  lookup the right-click menu and the Delete key resolve through. */
10609
10654
  const visibleLeaves = []
10610
10655
  if (favorites.length > 0) flatLeaves(forestOf(locals.filter((entry) => favorites.indexOf(entry.name) >= 0)), '', 0, visibleLeaves)
10611
10656
  flatLeaves(forestOf(locals), '', 0, visibleLeaves)
10612
- for (const entry of remotes) visibleLeaves.push({ entry: entry, label: undefined, depth: 0 })
10657
+ /* Same tree, same prefix namespace the section renders with — Shift-ranges
10658
+ and Ctrl+A must walk the EXACT order rows are shown in. */
10659
+ flatLeaves(forestOf(remotes.map((entry) => Object.assign({}, entry, { remote: true })), remoteNameOf), 'remote', 0, visibleLeaves)
10613
10660
  for (const entry of tags) visibleLeaves.push({ entry: entry, label: undefined, depth: 0 })
10614
10661
  visibleRef.current = visibleLeaves
10615
10662
  const entryBySelKey = new Map()
@@ -10726,7 +10773,7 @@ window.__ModuleLoader__.load({
10726
10773
  }, t('branches.head')),
10727
10774
  favorites.length === 0 ? null : section('favorites', t('branches.favorites'), locals.filter((entry) => favorites.indexOf(entry.name) >= 0), (entry) => entry),
10728
10775
  section('local', t('branches.local'), locals, (entry) => entry, true),
10729
- section('remote', t('branches.remote'), remotes, (entry) => Object.assign({}, entry, { remote: true })),
10776
+ section('remote', t('branches.remote'), remotes, (entry) => Object.assign({}, entry, { remote: true }), true, 'remote', remoteNameOf),
10730
10777
  section('tags', t('branches.tags'), tags, (entry) => Object.assign({}, entry, { tag: true }))),
10731
10778
  E(MarqueeBox, { box: treeMarquee.box }))
10732
10779
  }
@@ -12028,6 +12075,7 @@ window.__ModuleLoader__.load({
12028
12075
  useMeasureLayoutEffect(() => {
12029
12076
  const node = hostRef.current
12030
12077
  const holder = node === null || typeof node.closest !== 'function' ? null : node.closest('.dig-conversation-seat[data-immersive="1"]')
12078
+ if (gateNestedLoop() === false) return
12031
12079
  setImmersiveSeat(holder !== null)
12032
12080
  })
12033
12081
  const compact = size.width > 0 && size.width < COMPACT_MAX_WIDTH
@@ -12089,14 +12137,48 @@ window.__ModuleLoader__.load({
12089
12137
  re-clamps a stored size has to see the same numbers the drag did — a drag
12090
12138
  allowed further than the render allows snaps back under the cursor. The
12091
12139
  bail-out comparison is what keeps this from looping. */
12092
- useEffect(() => {
12140
+ /* A LAYOUT effect: on mount this runs before the first paint, so the
12141
+ fallback layout (paneGeometry's stylesheet/literal defaults) is
12142
+ replaced by the live one inside the same frame instead of being
12143
+ painted and then jumped a frame later. Painting both is the "height
12144
+ animation" on tab switches from issue #16 — two visible layouts ~80px
12145
+ apart read as a smooth resize in a low-fps screen recording. Later
12146
+ runs still track content arrival; the layout-phase cost is one
12147
+ querySelectorAll over the 3-4 panes the post-paint run already paid. */
12148
+ /* ---------- measurement loop fuse (issue #17) ---------- */
12149
+
12150
+ /* Every effect below runs on EVERY render and feeds a state update.
12151
+ In a steady UI the bail-out comparisons stop the chain, but when the
12152
+ realized layout disagrees with the just-measured one — a float pane
12153
+ being created/resized on the desktop client hit exactly this — each
12154
+ render re-measures a DIFFERENT box, the update nests synchronously
12155
+ and React aborts at depth 50 with #185, leaving the pane white
12156
+ behind the host's slot boundary. The gate detects that nesting
12157
+ directly: runs are counted in a 40 ms window and a nested render
12158
+ loop (50 commits inside one task) trips it long before React's
12159
+ limit, while legitimate per-event work (a pointermove drag renders
12160
+ at most twice in 40 ms) never comes close. Skipping a measurement
12161
+ during a pathological burst only freezes the affected number until
12162
+ the next window — degraded ratios for a frame, never a white pane. */
12163
+ const measureGate = useRef({ n: 0, at: 0 })
12164
+ const gateNestedLoop = () => {
12165
+ const slot = measureGate.current
12166
+ const now = Date.now()
12167
+ if (now - slot.at > 40) { slot.at = now; slot.n = 0 }
12168
+ slot.n += 1
12169
+ return slot.n <= 6
12170
+ }
12171
+
12172
+ useMeasureLayoutEffect(() => {
12093
12173
  const found = measurePanes()
12174
+ if (gateNestedLoop() === false) return
12094
12175
  setPaneSizes((current) => (samePaneSizes(current, found) ? current : found))
12095
12176
  })
12096
12177
  useEffect(() => {
12097
12178
  const node = hostRef.current
12098
12179
  const holder = node === null || typeof node.closest !== 'function' ? null : node.closest('[data-dockkit-float]')
12099
12180
  const next = holder === null ? null : holder.getAttribute('data-dockkit-float')
12181
+ if (gateNestedLoop() === false) return
12100
12182
  setFloatPaneId((current) => (current === next ? current : next))
12101
12183
  })
12102
12184
  const dock = props.dock === undefined ? null : props.dock
package/src/index.js CHANGED
@@ -755,7 +755,14 @@ async function branches(payload) {
755
755
  behind: 0,
756
756
  }
757
757
  if (parts[0].startsWith('refs/heads/')) local.push(entry)
758
- else if (parts[0].startsWith('refs/remotes/')) remote.push(entry)
758
+ else if (parts[0].startsWith('refs/remotes/')) {
759
+ /* "<remote>/HEAD" is the symbolic pointer at a remote's default branch —
760
+ its %(refname:short) collapses to the BARE remote name, so it is not a
761
+ branch anyone can check out. It never becomes a row (the stray
762
+ "<remote>" entry used to pollute the remote tree and, grouped, it also
763
+ defeated the tree's single-remote prefix strip). */
764
+ if (/^refs\/remotes\/[^/]+\/HEAD$/.test(parts[0]) === false) remote.push(entry)
765
+ }
759
766
  else if (parts[0].startsWith('refs/tags/')) tags.push({ name: entry.name, hash: entry.hash, shortHash: entry.shortHash, date: entry.date, subject: entry.subject })
760
767
  }
761
768
  for (const entry of local) {