dsh-ide-git 0.19.0 → 0.19.1

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.0",
3
+ "version": "0.19.1",
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.0",
3
+ "version": "0.19.1",
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
@@ -11035,7 +11035,7 @@ window.__ModuleLoader__.load({
11035
11035
  }, E(Icon, { name: 'more', size: 18 })))
11036
11036
  /* The selection action bar: visible only while a multi-select exists, so
11037
11037
  the mouse-only path (no right-click, no keyboard) keeps full power. */
11038
- const selectionBar = selection.size === 0 ? null : E('div', { className: 'dig-selection-bar' },
11038
+ const selectionBar = selection.size === 0 ? null : E('div', { className: 'dig-selection-bar', 'data-cordis-panel': '' },
11039
11039
  E('span', { className: 'dig-selection-count' }, fill(t('selection.count'), { n: selection.size })),
11040
11040
  props.onBulkStage === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onBulkStage() }, t('action.stage')),
11041
11041
  props.onBulkUnstage === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onBulkUnstage() }, t('action.unstage')),
@@ -11068,8 +11068,12 @@ window.__ModuleLoader__.load({
11068
11068
  visibleItems.map((item) => E(Fragment, { key: item.key }, item.node)),
11069
11069
  winLast < changeItems.length ? E('div', { key: 'pad-bottom', style: { height: String(changeOffsets[changeItems.length] - changeOffsets[winLast]) + 'px' } }) : null,
11070
11070
  ]),
11071
+ /* In-flow BETWEEN the list and the composer: a first selection
11072
+ shrinks the viewport bottom edge without moving the list content
11073
+ (scrollTop untouched), so the v0.18.3 dblclick precision holds —
11074
+ and nothing on the list (group head, rows) is ever covered. */
11075
+ selectionBar,
11071
11076
  composer),
11072
- selectionBar,
11073
11077
  marquee === null ? null : E('div', {
11074
11078
  className: 'dig-marquee',
11075
11079
  style: { left: marquee.x + 'px', top: marquee.y + 'px', width: marquee.w + 'px', height: marquee.h + 'px' },
@@ -12021,9 +12025,14 @@ window.__ModuleLoader__.load({
12021
12025
  roll live state back to a stale snapshot. */
12022
12026
  const cachedPage = readPaintPage(repoRoot)
12023
12027
  if (cachedPage !== undefined) {
12024
- if (summary === null) setSummary(cachedPage.summary)
12025
- if (branches === null) setBranches(cachedPage.branches)
12026
- if (commits.length === 0) { setCommits(cachedPage.commits); setHasMore(cachedPage.hasMore) }
12028
+ /* Field-wise: a pill may have painted only its summary (the pill
12029
+ merges its own two fields alone), so branches/commits can be
12030
+ absent from the page. Seeding undefined into state would crash
12031
+ the branch-tree render (undefined.remotes) — skip that field
12032
+ and let refresh() fetch it as usual. */
12033
+ if (summary === null && cachedPage.summary !== undefined) setSummary(cachedPage.summary)
12034
+ if (branches === null && cachedPage.branches !== undefined) setBranches(cachedPage.branches)
12035
+ if (commits.length === 0 && Array.isArray(cachedPage.commits)) { setCommits(cachedPage.commits); setHasMore(cachedPage.hasMore === true) }
12027
12036
  }
12028
12037
  void refresh()
12029
12038
  return undefined
@@ -13005,7 +13014,7 @@ window.__ModuleLoader__.load({
13005
13014
  }
13006
13015
 
13007
13016
  /* Remotes themselves, for "pull from" / "push to" style entries. */
13008
- const remoteNames = branches === null ? [] : branches.remotes.map((entry) => entry.name === undefined ? String(entry) : entry.name)
13017
+ const remoteNames = branches == null ? [] : branches.remotes.map((entry) => entry.name === undefined ? String(entry) : entry.name)
13009
13018
 
13010
13019
  const showWorkingDiff = () => {
13011
13020
  const changes = summary === null ? null : summary.changes
@@ -14123,12 +14132,26 @@ window.__ModuleLoader__.load({
14123
14132
  VIEWPORT-space .dig-changes (invariant 13: position:fixed would be
14124
14133
  captured by the workbench's contain:layout and flung off-panel). */
14125
14134
  '.dig-marquee{position:absolute;border:1px solid var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent);pointer-events:none;z-index:6}',
14126
- /* The selection bar FLOATS over the top of the list (absolute, solid
14127
- surface): as an in-flow bar it shoved the whole list down one row on
14128
- the first selection — the second click of every double-click then
14129
- landed on the shifted group head and the dblclick died (v0.18.3). */
14130
- '.dig-selection-bar{position:absolute;top:0;left:0;right:0;z-index:6;display:flex;align-items:center;gap:12px;padding:3px 10px;font-size:11px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));border-bottom:1px solid var(--dsw-alias-border-subtle,transparent);box-shadow:var(--dsw-elevation-prominent,0 4px 14px rgba(0,0,0,.3))}',
14131
- '.dig-selection-bar .dig-link{margin-left:0}',
14135
+ /* The selection action bar (v0.19.1 redesign). It used to FLOAT over the
14136
+ top of the list (absolute, v0.18.3): that covered the group head and
14137
+ the first rows, and on transparency skins (dsh-any-background) its
14138
+ background went see-through, so its own text blended into the rows
14139
+ underneath — unreadable. Now it is an IN-FLOW island between the list
14140
+ and the composer: nothing is covered, and a first selection only
14141
+ shrinks the viewport bottom edge (scrollTop untouched), which keeps
14142
+ the v0.18.3 dblclick precision. The surface rides the official menu
14143
+ token; data-cordis-panel (set at the render site) lets transparency
14144
+ skins sync it exactly like the official menus. */
14145
+ /* Local alias restore: dsh-any-background re-declares --dsw-alias-bg-layer-1/2/3
14146
+ on [data-conversation-composer-overlay] (the session seat) as its trajectory
14147
+ vars, which are absent when that feature is off — the seat inherits
14148
+ guaranteed-invalid values, and var() fallbacks do NOT rescue IVCT, so the
14149
+ whole background chain below resolves to transparent. Re-declaring the two
14150
+ aliases from the official static token (the exact source the light theme's
14151
+ own alias defs point at, theme-correct in dark too) makes the chain valid
14152
+ again regardless of any-bg's injection state. */
14153
+ '.dig-selection-bar{--dsw-alias-bg-layer-1:var(--dsw-static-neutral-bluish-00,#fff);--dsw-alias-bg-layer-2:var(--dsw-static-neutral-bluish-00,#fff);display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:5px 10px;margin:6px 0;font-size:11px;line-height:16px;border-radius:var(--dsw-radius-sm,8px);background:var(--dsh-any-op-menu-cordis,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-subtle,transparent));box-shadow:var(--dsw-elevation-prominent,0 4px 14px rgba(0,0,0,.25))}',
14154
+ '.dig-selection-bar .dig-link{margin-left:0;padding:2px 0}',
14132
14155
  '.dig-selection-bar .dig-link-danger{color:var(--dsw-alias-state-error-primary)}',
14133
14156
  '.dig-selection-count{color:var(--dsw-alias-label-secondary);white-space:nowrap}',
14134
14157
  /* Focusable lists keep their own selection highlight as the focus
@@ -14333,14 +14356,23 @@ window.__ModuleLoader__.load({
14333
14356
  '.dig-cs-behind{color:var(--dsw-alias-state-business-primary)}',
14334
14357
  '.dig-cs-ahead{color:var(--dsw-alias-state-success-primary)}',
14335
14358
  '.dig-cs-error{display:inline-flex;align-items:center;color:var(--dsw-alias-state-error-primary);font-size:12px;padding:0 3px;cursor:help}',
14336
- '.dig-cs-menu{position:absolute;top:calc(100% + 6px);left:0;z-index:60;min-width:220px;max-width:320px;max-height:320px;overflow:auto;padding:6px;border-radius:var(--dsw-radius-lg,12px);background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1)));backdrop-filter:var(--dsw-menu-backdrop-filter,blur(20px));box-shadow:var(--dsw-elevation-prominent,var(--dsw-elevation-dimmed,0 8px 24px rgba(0,0,0,.2)));color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px}',
14359
+ // .dig-cs-menu opens upward (the composer sits at the bottom of the UI). The surface
14360
+ // carries no backdrop-filter of its own: the data-cordis-panel attribute opts the popover
14361
+ // into dsh-any-background's input-surface channel (--dsw-specific-menu remapped to
14362
+ // var(--dsh-any-op-menu-cordis) on body — see the .dig-selection-bar note:
14363
+ // leading with the op var keeps the fill valid even when the plugin's
14364
+ // surface sampling misses and that remap would otherwise IVCT to transparent).
14365
+ // --dsh-any-op-menu-cordis at the input opacity tier, plus its input blur rule), so fill
14366
+ // opacity and frost follow the composer card exactly. Without that plugin the
14367
+ // theme-owned menu material applies.
14368
+ '.dig-cs-menu{--dsw-alias-bg-layer-1:var(--dsw-static-neutral-bluish-00,#fff);--dsw-alias-bg-layer-2:var(--dsw-static-neutral-bluish-00,#fff);position:absolute;bottom:calc(100% + 6px);left:0;z-index:60;min-width:220px;max-width:320px;max-height:320px;overflow:auto;padding:6px;border-radius:var(--dsw-radius-lg,12px);background:var(--dsh-any-op-menu-cordis,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-elevation-prominent,var(--dsw-elevation-dimmed,0 8px 24px rgba(0,0,0,.2)));color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px}',
14337
14369
  '.dig-cs-item{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;line-height:20px;cursor:pointer;text-align:left}',
14338
14370
  '.dig-cs-item:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
14339
14371
  '.dig-cs-item:disabled{opacity:.5;cursor:default}',
14340
14372
  '.dig-cs-item[data-current=true]{color:var(--dsw-alias-brand-primary)}',
14341
14373
  '.dig-cs-item svg{flex:none}',
14342
14374
  '.dig-cs-item-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
14343
- '.dig-cs-item-path{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-dimmed,var(--dsw-alias-label-tertiary));font-size:11px;max-width:140px}',
14375
+ '.dig-cs-item-path{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:11px;max-width:140px}',
14344
14376
  '.dig-cs-item-note{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}',
14345
14377
  '.dig-cs-item-counts{display:inline-flex;gap:5px;margin-left:auto;flex:none;font-variant-numeric:tabular-nums;font-size:11px}',
14346
14378
  '.dig-cs-static{color:var(--dsw-alias-label-tertiary);cursor:default}',
@@ -14480,14 +14512,68 @@ window.__ModuleLoader__.load({
14480
14512
  const COMPOSER_POLL_MS = 30000
14481
14513
  const COMPOSER_ERROR_TTL_MS = 6000
14482
14514
 
14515
+ /* Windows paths ride backslashes, so both pill labels cut the leaf with
14516
+ this slash-aware split instead of a bare forward-slash split — a drive
14517
+ path must read as the leaf name, not the whole path. */
14518
+ function baseNameOf(path) {
14519
+ if (typeof path !== 'string' || path === '') return ''
14520
+ const segments = path.split(/[\\/]/).filter((segment) => segment !== '')
14521
+ return segments.length > 0 ? segments[segments.length - 1] : path
14522
+ }
14523
+
14524
+ /* The pill feeds the SAME keep-warm cache the panel and the prewarmer
14525
+ use (paintRepos / paintPage): a workspace the user has visited — by
14526
+ panel, by pill, or by the session-switch prewarm — re-paints the pill
14527
+ on mount with zero round trips, so switching sessions swaps the text
14528
+ instead of tearing the whole component down and back. The pill only
14529
+ ever MERGES its two fields into an existing page; a panel page with
14530
+ commits must not lose them to a pill poll. */
14531
+ function mergePaintPage(repoRoot, patch) {
14532
+ const entry = readPaintPage(repoRoot)
14533
+ const page = {}
14534
+ if (entry !== undefined) {
14535
+ if (entry.summary !== undefined) page.summary = entry.summary
14536
+ if (entry.branches !== undefined) page.branches = entry.branches
14537
+ if (entry.commits !== undefined) page.commits = entry.commits
14538
+ if (entry.hasMore !== undefined) page.hasMore = entry.hasMore
14539
+ }
14540
+ Object.assign(page, patch)
14541
+ writePaintPage(repoRoot, page)
14542
+ }
14543
+
14544
+ /* The pill's repo pick order (remembered → workspace root when it is a
14545
+ repository → first row), factored out so the cached seed and the fresh
14546
+ discovery apply identically. */
14547
+ function composerPickRepoOf(sessionId, data) {
14548
+ const remembered = readRememberedRepo(sessionId)
14549
+ const paths = data.repos.map((repo) => repo.path)
14550
+ if (remembered !== undefined && paths.indexOf(remembered) >= 0) return remembered
14551
+ if (data.isRepo === true) return data.cwd
14552
+ if (paths.length > 0) return paths[0]
14553
+ return null
14554
+ }
14555
+
14483
14556
  function ComposerGitStatus(props) {
14484
14557
  const ctx = props.ctx
14485
14558
  const t = props.t
14486
14559
  const sessionId = typeof props.sessionId === 'string' && props.sessionId !== '' ? props.sessionId : 'default'
14487
14560
  const [enabled, setEnabled] = useState(() => composerStatusEnabled())
14488
14561
  const [cwd, setCwd] = useState(() => sessionCwdOf(ctx, sessionId))
14489
- const [repoState, setRepoState] = useState(null)
14490
- const [repoRoot, setRepoRoot] = useState(null)
14562
+ /* Warm first paint: a workspace the panel, the pill or the prewarmer
14563
+ has already visited starts from the keep-warm cache, so a session
14564
+ switch swaps text instead of flashing through an empty pill. */
14565
+ const [repoState, setRepoState] = useState(() => {
14566
+ const cwd0 = sessionCwdOf(ctx, sessionId)
14567
+ if (cwd0 === undefined) return null
14568
+ const cached = readPaintRepos(cwd0)
14569
+ return cached === undefined ? null : cached
14570
+ })
14571
+ const [repoRoot, setRepoRoot] = useState(() => {
14572
+ const cwd0 = sessionCwdOf(ctx, sessionId)
14573
+ if (cwd0 === undefined) return null
14574
+ const cached = readPaintRepos(cwd0)
14575
+ return cached === undefined ? null : composerPickRepoOf(sessionId, cached)
14576
+ })
14491
14577
  const [summary, setSummary] = useState(null)
14492
14578
  const [menu, setMenu] = useState(null)
14493
14579
  const [branchList, setBranchList] = useState(null)
@@ -14527,19 +14613,24 @@ window.__ModuleLoader__.load({
14527
14613
  useEffect(() => {
14528
14614
  if (cwd === undefined || enabled !== true) { setRepoState(null); setRepoRoot(null); setSummary(null); return undefined }
14529
14615
  let cancelled = false
14616
+ const applyRepos = (data) => {
14617
+ setRepoState(data)
14618
+ setRepoRoot(composerPickRepoOf(sessionId, data))
14619
+ }
14620
+ /* Warm seed first: a workspace the panel, the pill or the prewarmer
14621
+ already visited paints its labels this tick; the request below
14622
+ silently re-proves them. A failed refresh must not evict a good
14623
+ seed — only fall back to the empty state when nothing is on
14624
+ screen. */
14625
+ const seeded = readPaintRepos(cwd)
14626
+ if (seeded !== undefined) applyRepos(seeded)
14530
14627
  void (async () => {
14531
14628
  try {
14532
14629
  const data = await request('repos', { cwd: cwd })
14533
14630
  if (cancelled === true) return
14534
- setRepoState(data)
14535
- const remembered = readRememberedRepo(sessionId)
14536
- const paths = data.repos.map((repo) => repo.path)
14537
- let pick = null
14538
- if (remembered !== undefined && paths.indexOf(remembered) >= 0) pick = remembered
14539
- else if (data.isRepo === true) pick = data.cwd
14540
- else if (paths.length > 0) pick = paths[0]
14541
- setRepoRoot(pick)
14542
- } catch (error) { void error; if (cancelled !== true) setRepoState({ repos: [], isRepo: false }) }
14631
+ writePaintRepos(cwd, data)
14632
+ applyRepos(data)
14633
+ } catch (error) { void error; if (cancelled !== true && seeded === undefined) setRepoState({ repos: [], isRepo: false }) }
14543
14634
  })()
14544
14635
  return () => { cancelled = true }
14545
14636
  }, [cwd, sessionId, enabled])
@@ -14555,8 +14646,15 @@ window.__ModuleLoader__.load({
14555
14646
  const data = await request('summary', { cwd: cwd, repoRoot: repoRoot, stats: false })
14556
14647
  if (cancelled === true) return
14557
14648
  setSummary(data)
14649
+ /* Stale-while-revalidate both ways: our poll keeps the warm page
14650
+ (panel / prewarm / seed) fresh, and a warm page seeds the NEXT
14651
+ mount with zero round trips. Merge only our field — a panel
14652
+ page keeps its commits. */
14653
+ mergePaintPage(repoRoot, { summary: data })
14558
14654
  } catch (error) { void error }
14559
14655
  }
14656
+ const seed = readPaintPage(repoRoot)
14657
+ if (seed !== undefined && seed.summary !== undefined && seed.summary !== null) setSummary(seed.summary)
14560
14658
  void load()
14561
14659
  const timer = setInterval(() => { void load() }, COMPOSER_POLL_MS)
14562
14660
  return () => { cancelled = true; clearInterval(timer) }
@@ -14568,7 +14666,7 @@ window.__ModuleLoader__.load({
14568
14666
  const watcher = (sid, root) => {
14569
14667
  if (sid !== sessionId || typeof root !== 'string' || root === '') return
14570
14668
  setRepoRoot((current) => (current === root ? current : root))
14571
- setSummary(null)
14669
+ setSummary(seedSummaryOf(root))
14572
14670
  setBranchList(null)
14573
14671
  setTick((value) => value + 1)
14574
14672
  }
@@ -14610,7 +14708,12 @@ window.__ModuleLoader__.load({
14610
14708
  if (data === null || (data.isRepo !== true && (data.repos === undefined || data.repos.length === 0))) return null
14611
14709
  const repoPaths = data.repos.map((repo) => repo.path)
14612
14710
  const singleRepo = repoPaths.length <= 1
14613
- const repoName = repoRoot === null ? '' : (repoRoot.split('/').filter((segment) => segment !== '').pop() || repoRoot)
14711
+ /* The pill shows the repository's OWN name — the origin URL's leaf
14712
+ (summary.origin) — so a clone renamed on disk still reads as the
14713
+ repo it came from; the folder basename is only the fallback for
14714
+ repos without any remote. */
14715
+ const originName = summary !== null && typeof summary.origin === 'string' && summary.origin !== '' ? summary.origin : ''
14716
+ const repoName = originName !== '' ? originName : baseNameOf(repoRoot)
14614
14717
  const branchName = summary !== null && typeof summary.branch === 'string' && summary.branch !== '' ? summary.branch : null
14615
14718
  const detached = summary !== null && summary.detached === true
14616
14719
  const ahead = summary !== null && typeof summary.ahead === 'number' ? summary.ahead : 0
@@ -14621,12 +14724,17 @@ window.__ModuleLoader__.load({
14621
14724
 
14622
14725
  const openBranchMenu = async () => {
14623
14726
  setMenu('branch')
14624
- setBranchList(null)
14625
- setBranchLoading(true)
14727
+ /* A warm page (panel or prewarm) fills the list this tick; the fetch
14728
+ below re-proves it. On failure keep whatever is showing. */
14729
+ const seed = readPaintPage(repoRoot)
14730
+ const seeded = seed !== undefined && seed.branches !== undefined && seed.branches !== null ? seed.branches : null
14731
+ setBranchList(seeded)
14732
+ setBranchLoading(seeded === null)
14626
14733
  try {
14627
14734
  const branchesData = await request('branches', { cwd: cwd, repoRoot: repoRoot })
14628
14735
  setBranchList(branchesData)
14629
- } catch (caught) { void caught; setBranchList(null) } finally { setBranchLoading(false) }
14736
+ mergePaintPage(repoRoot, { branches: branchesData })
14737
+ } catch (caught) { void caught } finally { setBranchLoading(false) }
14630
14738
  }
14631
14739
  const checkoutBranch = async (name) => {
14632
14740
  if (repoRoot === null || checkingOut !== '') return
@@ -14641,10 +14749,18 @@ window.__ModuleLoader__.load({
14641
14749
  setError(caught instanceof Error ? caught.message : String(caught))
14642
14750
  } finally { setCheckingOut('') }
14643
14751
  }
14752
+ /* Swap to a warm summary synchronously when one exists — the null gap
14753
+ between pick and effect would paint one empty frame. */
14754
+ const seedSummaryOf = (root) => {
14755
+ if (typeof root !== 'string' || root === '') return null
14756
+ const seed = readPaintPage(root)
14757
+ return seed !== undefined && seed.summary !== undefined && seed.summary !== null ? seed.summary : null
14758
+ }
14759
+
14644
14760
  const pickRepoRoot = (path) => {
14645
14761
  setMenu(null)
14646
14762
  setRepoRoot(path)
14647
- setSummary(null)
14763
+ setSummary(seedSummaryOf(path))
14648
14764
  setBranchList(null)
14649
14765
  setTick((value) => value + 1)
14650
14766
  rememberRepoShared(sessionId, path)
@@ -14676,7 +14792,7 @@ window.__ModuleLoader__.load({
14676
14792
  behind > 0 ? E('span', { className: 'dig-cs-badge dig-cs-behind', title: t('composer.behindHint') }, '↓' + String(behind)) : null,
14677
14793
  ahead > 0 ? E('span', { className: 'dig-cs-badge dig-cs-ahead', title: t('composer.aheadHint') }, '↑' + String(ahead)) : null,
14678
14794
  error !== '' ? E('span', { className: 'dig-cs-error', title: error }, '×') : null,
14679
- menu === 'repo' ? E('div', { className: 'dig-cs-menu' },
14795
+ menu === 'repo' ? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
14680
14796
  repoPaths.map((path) => E('button', {
14681
14797
  key: path,
14682
14798
  type: 'button',
@@ -14686,12 +14802,12 @@ window.__ModuleLoader__.load({
14686
14802
  onClick: () => pickRepoRoot(path),
14687
14803
  },
14688
14804
  E(Icon, { name: 'commit', size: 11 }),
14689
- E('span', { className: 'dig-cs-item-name' }, path.split('/').filter((segment) => segment !== '').pop() || path),
14805
+ E('span', { className: 'dig-cs-item-name' }, baseNameOf(path)),
14690
14806
  E('span', { className: 'dig-cs-item-path' }, path))))
14691
14807
  : null,
14692
14808
  menu === 'branch' ? (branchLoading === true || branchList === null
14693
- ? E('div', { className: 'dig-cs-menu' }, E('div', { className: 'dig-cs-item dig-cs-static' }, '…'))
14694
- : E('div', { className: 'dig-cs-menu' },
14809
+ ? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' }, E('div', { className: 'dig-cs-item dig-cs-static' }, '…'))
14810
+ : E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
14695
14811
  branchList.local.map((entry) => E('button', {
14696
14812
  key: entry.name,
14697
14813
  type: 'button',
package/src/index.js CHANGED
@@ -600,6 +600,35 @@ async function operationOf(root) {
600
600
 
601
601
  /* ============================== methods ============================== */
602
602
 
603
+ /* The repository's own name, cut from its origin URL — a cloned folder that
604
+ was renamed on disk still reports the name the remote gave it (the leaf of
605
+ the URL, minus a trailing .git). scp-style (user@host:path), URL-style and
606
+ plain local paths all cut on the last slash or colon. */
607
+ function repoNameFromUrl(url) {
608
+ if (typeof url !== 'string' || url.trim() === '') return ''
609
+ const tail = url.trim().replace(/\/+$/, '').replace(/\.git$/i, '')
610
+ const segments = tail.split(/[\\/:]/).filter((segment) => segment !== '')
611
+ return segments.length > 0 ? segments[segments.length - 1] : ''
612
+ }
613
+ /* origin first, then the first configured remote, then '' — the client
614
+ falls back to the folder basename when this is empty. Both reads are
615
+ local config reads, never network. */
616
+ async function originNameOf(root) {
617
+ try {
618
+ const origin = await git(root, ['remote', 'get-url', 'origin'])
619
+ const name = repoNameFromUrl(origin)
620
+ if (name !== '') return name
621
+ } catch (error) { void error }
622
+ try {
623
+ const remotesOut = await runGit(root, ['remote'])
624
+ if (remotesOut.code !== 0) return ''
625
+ const first = remotesOut.stdout.split('\n').map((line) => line.trim()).filter((line) => line !== '')[0]
626
+ if (first === undefined) return ''
627
+ const urlOut = await runGit(root, ['remote', 'get-url', first])
628
+ return urlOut.code === 0 ? repoNameFromUrl(urlOut.stdout.trim()) : ''
629
+ } catch (error) { void error; return '' }
630
+ }
631
+
603
632
  async function summary(payload) {
604
633
  const cwd = cwdOf(payload)
605
634
  const root = await repoRootOf(cwd, payload)
@@ -614,7 +643,7 @@ async function summary(payload) {
614
643
  large `status` scan no longer serialises behind five cheap calls
615
644
  (issue #11: the sum of ~ten sequential commands became the max of two
616
645
  chains). */
617
- const [branchInfo, statusOut, unstagedStat, stagedStat, ignored, stashCount, worktrees, operation, head] = await Promise.all([
646
+ const [branchInfo, statusOut, unstagedStat, stagedStat, ignored, stashCount, worktrees, operation, head, originName] = await Promise.all([
618
647
  (async () => {
619
648
  const branch = await currentBranchOf(root)
620
649
  const detached = branch === 'HEAD'
@@ -633,6 +662,7 @@ async function summary(payload) {
633
662
  checkouts — the panel's own actions already refresh everything, but
634
663
  nothing else watches the repo. Empty string on an unborn branch. */
635
664
  git(root, ['rev-parse', 'HEAD']).then((out) => out.trim()).catch(() => ''),
665
+ originNameOf(root),
636
666
  ])
637
667
  const groups = classify(parsePorcelain(statusOut))
638
668
  return {
@@ -644,6 +674,9 @@ async function summary(payload) {
644
674
  ahead: branchInfo.tracking.ahead,
645
675
  behind: branchInfo.tracking.behind,
646
676
  head: head,
677
+ /* The repository's own name (v0.19.1): the origin URL's leaf, so a
678
+ renamed clone still shows the real repo name, not the folder's. */
679
+ origin: originName,
647
680
  changes: {
648
681
  staged: decorate(groups.staged, stagedStat),
649
682
  unstaged: decorate(groups.unstaged, unstagedStat),