dsh-ide-git 0.5.2 → 0.5.4

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.5.2",
3
+ "version": "0.5.4",
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.5.2",
3
+ "version": "0.5.4",
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
@@ -34,6 +34,10 @@ window.__ModuleLoader__.load({
34
34
  locale registry. */
35
35
  const LOCALE_NS = 'dsh-ide-git'
36
36
  const LANE_WIDTH = 14
37
+ /* Hard cap on the drawn lane columns. Lanes beyond the cap collapse into
38
+ the last column: without the cap a branchy repository pushed the graph
39
+ wider than the panel and shoved the subject column out of view. */
40
+ const GRAPH_MAX_LANES = 7
37
41
  const LANE_COLORS = ['#4d6bfe', '#e2a03f', '#3fb950', '#d2679b', '#59b0d6', '#b083f0', '#d2694a', '#8a9aa8']
38
42
  const AUTO_REFRESH_MS = 12000
39
43
  const REPO_KEY = 'dsh-ide-git.repo.v1'
@@ -3521,6 +3525,16 @@ window.__ModuleLoader__.load({
3521
3525
  /* ============================== commit graph ============================== */
3522
3526
 
3523
3527
  function buildRows(commits) {
3528
+ /* The log is a WINDOW: the oldest visible commits usually have parents
3529
+ that were never loaded. A parent outside the window must not park its
3530
+ hash on a lane — the lane would stay occupied for the rest of the
3531
+ page (the hash can never be consumed), every later branch tip would
3532
+ claim yet another fresh lane, and the drawing degenerated into a
3533
+ bundle of independent parallel lines that pushed the subjects out of
3534
+ the panel. So the pass first learns which hashes are actually on the
3535
+ board, and an edge to anything else simply ends at its commit. */
3536
+ const present = new Set()
3537
+ for (const commit of commits) present.add(commit.hash)
3524
3538
  const lanes = []
3525
3539
  const rows = []
3526
3540
  for (const commit of commits) {
@@ -3538,24 +3552,30 @@ window.__ModuleLoader__.load({
3538
3552
  a branch that merged back into a lane to its left kept a stub of its
3539
3553
  own colour running on above the junction — a fork that never
3540
3554
  happened. Only a parent that is not on the board yet inherits the
3541
- lane, which is what keeps an ordinary line continuous. */
3555
+ lane, and (v0.5.4) only a parent that is on the board at all: a
3556
+ parent past the loaded window ends the lane at this row instead of
3557
+ holding it for a commit that will never arrive. */
3542
3558
  if (parents.length > 0) {
3543
3559
  const first = lanes.indexOf(parents[0])
3544
3560
  if (first !== -1 && first !== lane) {
3545
3561
  lanes[lane] = null
3546
3562
  parentLanes.push(first)
3547
- } else {
3563
+ } else if (present.has(parents[0])) {
3548
3564
  lanes[lane] = parents[0]
3565
+ } else {
3566
+ lanes[lane] = null
3549
3567
  }
3550
3568
  } else {
3551
3569
  lanes[lane] = null
3552
3570
  }
3553
3571
  for (let index = 1; index < parents.length; index += 1) {
3554
- let slot = lanes.indexOf(parents[index])
3572
+ const parent = parents[index]
3573
+ if (!present.has(parent)) continue
3574
+ let slot = lanes.indexOf(parent)
3555
3575
  if (slot === -1) {
3556
3576
  slot = lanes.indexOf(null)
3557
- if (slot === -1) { lanes.push(parents[index]); slot = lanes.length - 1 }
3558
- else lanes[slot] = parents[index]
3577
+ if (slot === -1) { lanes.push(parent); slot = lanes.length - 1 }
3578
+ else lanes[slot] = parent
3559
3579
  }
3560
3580
  parentLanes.push(slot)
3561
3581
  }
@@ -3576,10 +3596,12 @@ window.__ModuleLoader__.load({
3576
3596
  function GraphCell(props) {
3577
3597
  const row = props.row
3578
3598
  const height = props.height
3579
- const width = Math.max(1, row.width) * LANE_WIDTH + 8
3599
+ const width = Math.min(Math.max(1, row.width), GRAPH_MAX_LANES) * LANE_WIDTH + 8
3580
3600
  const middle = height / 2
3581
3601
  const children = []
3582
- const x = (lane) => 8 + lane * LANE_WIDTH
3602
+ /* Lanes beyond the cap share the last column: the geometry stays bounded
3603
+ and the subject column keeps its room. */
3604
+ const x = (lane) => 8 + Math.min(lane, GRAPH_MAX_LANES - 1) * LANE_WIDTH
3583
3605
  const color = (lane) => LANE_COLORS[lane % LANE_COLORS.length]
3584
3606
  const depth = Math.max(row.before.length, row.after.length)
3585
3607
  for (let lane = 0; lane < depth; lane += 1) {
@@ -3898,20 +3920,37 @@ window.__ModuleLoader__.load({
3898
3920
 
3899
3921
  function RepoSelect(props) {
3900
3922
  const t = props.t
3923
+ /* While the workspace is still being scanned the panel used to print "no
3924
+ repository found" — a verdict nobody had reached yet. A deep workspace
3925
+ scan takes seconds, so that premature verdict sat on screen long enough
3926
+ to look like the final answer. Loading shows loading; "not found" only
3927
+ shows once the scan came back empty. */
3928
+ if (props.loading === true) {
3929
+ return E('span', { className: 'dig-repo-static' },
3930
+ E(Icon, { name: 'folder', size: 12 }),
3931
+ E('span', { className: 'dig-repo-name' }, t('status.loading')))
3932
+ }
3901
3933
  if (props.repos.length <= 1) {
3902
3934
  const only = props.repos.length === 1 ? props.repos[0] : null
3903
3935
  return E('span', { className: 'dig-repo-static', title: only === null ? '' : only.path },
3904
3936
  E(Icon, { name: 'folder', size: 12 }),
3905
3937
  E('span', { className: 'dig-repo-name' }, only === null ? t('repo.empty') : only.name))
3906
3938
  }
3939
+ /* The switcher is a button over a panel-internal menu, not a native
3940
+ <select>: the OS-drawn popup ignores the panel theme entirely (light
3941
+ system colours over a dark translucent panel) and was unreadable. */
3942
+ let current = null
3943
+ for (const repo of props.repos) { if (repo.path === props.value) { current = repo; break } }
3907
3944
  return E('span', { className: 'dig-select-wrap', title: t('repo.switch') },
3908
3945
  E(Icon, { name: 'folder', size: 12 }),
3909
- E('select', {
3910
- className: 'dig-select dig-select-repo',
3911
- value: props.value === null ? '' : props.value,
3912
- onChange: (event) => props.onChange(event.target.value),
3913
- }, props.repos.map((repo) => E('option', { key: repo.path, value: repo.path },
3914
- repo.name + (repo.branch === null || repo.branch === undefined ? '' : ' · ' + repo.branch)))))
3946
+ E('button', {
3947
+ type: 'button',
3948
+ className: 'dig-select dig-select-btn dig-select-repo',
3949
+ onClick: props.onOpenMenu,
3950
+ },
3951
+ E('span', { className: 'dig-repo-name' },
3952
+ current === null ? t('repo.pick') : current.name + (current.branch === null || current.branch === undefined ? '' : ' · ' + current.branch)),
3953
+ E(Icon, { name: 'chevron', size: 12 })))
3915
3954
  }
3916
3955
 
3917
3956
  function RepoPicker(props) {
@@ -4550,7 +4589,10 @@ window.__ModuleLoader__.load({
4550
4589
  }, [repoRoot])
4551
4590
 
4552
4591
  useEffect(() => {
4553
- if (cwd === undefined) return undefined
4592
+ /* No workspace attached: there is nothing to scan, so land the empty
4593
+ result straight away instead of leaving repoState null (which reads
4594
+ as "still loading" and would spin forever). */
4595
+ if (cwd === undefined) { setRepoState({ repos: [], isRepo: false }); return undefined }
4554
4596
  let cancelled = false
4555
4597
  void (async () => {
4556
4598
  try {
@@ -4860,15 +4902,47 @@ window.__ModuleLoader__.load({
4860
4902
  const repoOptions = repoState === null ? [] : repoState.repos
4861
4903
  const dirty = summary === null ? 0 : summary.changes.staged.length + summary.changes.unstaged.length + summary.changes.untracked.length + summary.changes.conflicted.length
4862
4904
 
4905
+ /* Both header switchers are menu-driven now (see RepoSelect): the
4906
+ popovers reuse openMenuAt, so they clamp to the panel box and share
4907
+ the opaque, frosted menu surface instead of the OS select popup. */
4908
+ const openRepoMenu = useCallback((event) => {
4909
+ openMenuAt(event, repoOptions.map((repo) => ({
4910
+ id: 'repo:' + repo.path,
4911
+ icon: 'folder',
4912
+ tone: 'accent',
4913
+ label: repo.name + (repo.branch === null || repo.branch === undefined ? '' : ' · ' + repo.branch),
4914
+ active: repo.path === repoRoot,
4915
+ run: () => { if (repo.path !== repoRoot) pickRepo(repo.path) },
4916
+ })))
4917
+ }, [openMenuAt, repoOptions, repoRoot, pickRepo])
4918
+
4919
+ const openBranchSwitchMenu = useCallback((event) => {
4920
+ const local = branches === null ? [] : branches.local
4921
+ openMenuAt(event, local.map((entry) => ({
4922
+ id: 'switch:' + entry.name,
4923
+ icon: entry.head === true ? 'star' : 'branch',
4924
+ tone: entry.head === true ? 'warn' : 'accent',
4925
+ label: entry.name,
4926
+ active: entry.head === true,
4927
+ run: () => { if (entry.head !== true) void checkout(entry) },
4928
+ })))
4929
+ }, [openMenuAt, branches, checkout])
4930
+
4863
4931
  const topBar = E('div', { className: 'dig-topbar' },
4864
- E(RepoSelect, { t: t, repos: repoOptions, value: repoRoot, onChange: pickRepo }),
4932
+ E(RepoSelect, {
4933
+ t: t, repos: repoOptions, value: repoRoot,
4934
+ loading: repoState === null,
4935
+ onOpenMenu: openRepoMenu,
4936
+ }),
4865
4937
  branches === null || branches.local.length === 0 ? null : E('span', { className: 'dig-select-wrap', title: t('branch.switch') },
4866
4938
  E(Icon, { name: 'branch', size: 12 }),
4867
- E('select', {
4868
- className: 'dig-select dig-select-branch',
4869
- value: branches.branch,
4870
- onChange: (event) => { void run('checkout', { branch: event.target.value }) },
4871
- }, branches.local.map((entry) => E('option', { key: entry.name, value: entry.name }, entry.name)))),
4939
+ E('button', {
4940
+ type: 'button',
4941
+ className: 'dig-select dig-select-btn dig-select-branch',
4942
+ onClick: openBranchSwitchMenu,
4943
+ },
4944
+ E('span', { className: 'dig-repo-name' }, branches.branch),
4945
+ E(Icon, { name: 'chevron', size: 12 }))),
4872
4946
  summary === null || summary.upstream === null ? null : E('span', { className: 'dig-track' },
4873
4947
  (summary.ahead > 0 ? '↑' + summary.ahead : '') + (summary.behind > 0 ? ' ↓' + summary.behind : '')),
4874
4948
  operation === null ? null : E('span', { className: 'dig-opchip', title: t('operation.hint') }, t('operation.' + operation)),
@@ -5098,7 +5172,12 @@ window.__ModuleLoader__.load({
5098
5172
  /* ---------- body per chrome ---------- */
5099
5173
 
5100
5174
  let body
5101
- if (repoRoot === null) {
5175
+ if (repoState === null) {
5176
+ /* The scan is still running: a neutral loading state, never the
5177
+ "no repository found" verdict (that text is reserved for a scan
5178
+ that actually came back empty). */
5179
+ body = E('div', { className: 'dig-body' }, E('div', { className: 'dig-empty' }, t('status.loading')))
5180
+ } else if (repoRoot === null) {
5102
5181
  body = E('div', { className: 'dig-body' }, E(RepoPicker, {
5103
5182
  t: t, cwd: cwd === undefined ? '' : cwd, repos: repoOptions, onPick: pickRepo,
5104
5183
  }))
@@ -5282,7 +5361,10 @@ window.__ModuleLoader__.load({
5282
5361
  '.dig-select{appearance:none;-webkit-appearance:none;background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);font:inherit;font-weight:500;padding:1px 6px;height:22px;max-width:190px;min-width:0;cursor:pointer;text-overflow:ellipsis}',
5283
5362
  '.dig-select:hover{background:var(--dsw-alias-interactive-bg-hover)}',
5284
5363
  '.dig-select:focus{outline:1px solid var(--dsw-alias-brand-primary);outline-offset:-1px}',
5364
+ '.dig-select-btn{display:inline-flex;align-items:center;gap:4px;text-align:left;white-space:nowrap;overflow:hidden}',
5365
+ '.dig-select-btn .dig-repo-name{flex:1;min-width:0;max-width:none}',
5285
5366
  '.dig-select-repo{max-width:200px}',
5367
+ '.dig-select option,.dig-filter-select option{background:var(--dsw-alias-bg-layer-1,#22262e);color:var(--dsw-alias-label-primary,#e6e8ee)}',
5286
5368
  '.dig-select-branch{max-width:150px}',
5287
5369
  '.dig-repo-static{display:inline-flex;align-items:center;gap:5px;min-width:0;color:var(--dsw-alias-label-secondary)}',
5288
5370
  '.dig-repo-name{color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}',
@@ -5478,7 +5560,7 @@ window.__ModuleLoader__.load({
5478
5560
  '.dig-dialog-text{color:var(--dsw-alias-label-secondary);white-space:pre-wrap}',
5479
5561
  '.dig-dialog-actions{display:flex;justify-content:flex-end;gap:8px}',
5480
5562
  '.dig-dialog-actions .dig-btn-primary{margin-left:0}',
5481
- '.dig-menu{position:absolute;z-index:70;min-width:200px;max-width:calc(100% - 8px);max-height:calc(100% - 8px);overflow:auto;overscroll-behavior:contain;padding:4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));border:1px solid var(--dsw-alias-border-l2);box-shadow:0 10px 28px rgba(0,0,0,.35);display:flex;flex-direction:column}',
5563
+ '.dig-menu{position:absolute;z-index:70;min-width:200px;max-width:calc(100% - 8px);max-height:calc(100% - 8px);overflow:auto;overscroll-behavior:contain;padding:4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,blur(14px) saturate(1.2));backdrop-filter:var(--dsh-any-blur-card-panels,blur(14px) saturate(1.2));border:1px solid var(--dsw-alias-border-l2);box-shadow:0 10px 28px rgba(0,0,0,.35);display:flex;flex-direction:column}',
5482
5564
  '.dig-menu-item{display:flex;align-items:center;gap:8px;padding:4px 8px;border:none;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-weight:500;text-align:left;border-radius:5px;cursor:pointer;white-space:nowrap;overflow:hidden}',
5483
5565
  '.dig-menu-icon{display:inline-flex;flex:none}',
5484
5566
  '.dig-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}',
@@ -5565,45 +5647,63 @@ window.__ModuleLoader__.load({
5565
5647
  right-sidebar seats are the fallback for a host running this plugin on
5566
5648
  its own — registering both would draw the same panel twice in one
5567
5649
  column, so a late-arriving better-sidebar takes the native one down. */
5568
- let disposeNative = null
5650
+ let nativeFiber = null
5569
5651
  let hostedByBetterSidebar = false
5570
5652
 
5653
+ /* WAIT for both native seats instead of probing with ctx.get(): on a
5654
+ host without better-sidebar this apply can run BEFORE ui-sidebar-right
5655
+ provides the registry, and the old synchronous probe returned
5656
+ silently — the native seats were then never registered (reproduced
5657
+ on a clean instance and on the desktop app: the right sidebar showed
5658
+ no Git card and the console stayed empty). ctx.inject() retries the
5659
+ registration the moment the services appear. */
5571
5660
  const hostNatively = () => {
5572
- if (disposeNative !== null) return
5573
- const tabs = ctx.get('sidebarRightTabs')
5574
- const slots = ctx.get('slots')
5575
- if (tabs === undefined || typeof tabs.register !== 'function') return
5576
- if (slots === undefined || typeof slots.inject !== 'function') return
5577
- const offType = tabs.register({
5578
- id: NATIVE_ID,
5579
- kind: NATIVE_KIND,
5580
- title: () => t('title'),
5581
- guide: [{
5582
- id: 'git',
5583
- order: 21,
5584
- title: () => t('title'),
5585
- description: () => t('description'),
5586
- icon: (iconProps) => E(Icon, {
5587
- name: 'commit',
5588
- size: iconProps === undefined || iconProps.size === undefined ? 16 : iconProps.size,
5589
- }),
5590
- }],
5661
+ if (nativeFiber !== null) return
5662
+ nativeFiber = ctx.inject(['sidebarRightTabs', 'slots'], (nativeCtx) => {
5663
+ const tabs = nativeCtx.get('sidebarRightTabs')
5664
+ const slots = nativeCtx.get('slots')
5665
+ if (tabs === undefined || typeof tabs.register !== 'function') return
5666
+ if (slots === undefined || typeof slots.inject !== 'function') return
5667
+ nativeCtx.effect(() => {
5668
+ const offType = tabs.register({
5669
+ id: NATIVE_ID,
5670
+ kind: NATIVE_KIND,
5671
+ title: () => t('title'),
5672
+ guide: [{
5673
+ id: 'git',
5674
+ order: 21,
5675
+ title: () => t('title'),
5676
+ description: () => t('description'),
5677
+ icon: (iconProps) => E(Icon, {
5678
+ name: 'commit',
5679
+ size: iconProps === undefined || iconProps.size === undefined ? 16 : iconProps.size,
5680
+ }),
5681
+ }],
5682
+ })
5683
+ const offBody = slots.inject('sidebar.right.pane.tab', () => slots.register(
5684
+ { name: 'sidebar.right.pane.tab', key: NATIVE_ID },
5685
+ (tabProps) => E(NativePanel, Object.assign({}, tabProps, { t: t, ctx: ctx })),
5686
+ ))
5687
+ return () => {
5688
+ try { offBody() } catch (error) { void error }
5689
+ try { offType() } catch (error) { void error }
5690
+ }
5691
+ }, 'dsh-ide-git: native right-sidebar seats')
5591
5692
  })
5592
- const offBody = slots.inject('sidebar.right.pane.tab', () => slots.register(
5593
- { name: 'sidebar.right.pane.tab', key: NATIVE_ID },
5594
- (tabProps) => E(NativePanel, Object.assign({}, tabProps, { t: t, ctx: ctx })),
5595
- ))
5596
- disposeNative = () => {
5597
- try { offBody() } catch (error) { void error }
5598
- try { offType() } catch (error) { void error }
5599
- }
5693
+ }
5694
+
5695
+ const takeNativeDown = () => {
5696
+ if (nativeFiber === null) return
5697
+ const fiber = nativeFiber
5698
+ nativeFiber = null
5699
+ void Promise.resolve(fiber.dispose()).catch((error) => { void error })
5600
5700
  }
5601
5701
 
5602
5702
  ctx.inject(['betterSidebar'], (tabCtx) => {
5603
5703
  const betterSidebar = tabCtx.get('betterSidebar')
5604
5704
  if (betterSidebar === undefined || typeof betterSidebar.registerTab !== 'function') return
5605
5705
  hostedByBetterSidebar = true
5606
- if (disposeNative !== null) { disposeNative(); disposeNative = null }
5706
+ takeNativeDown()
5607
5707
  tabCtx.effect(() => {
5608
5708
  const offTab = betterSidebar.registerTab({
5609
5709
  id: TAB_ID,