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 +1 -1
- package/package.json +1 -1
- package/src/client.js +151 -51
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-ide-git",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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(
|
|
3558
|
-
else lanes[slot] =
|
|
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
|
-
|
|
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('
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
},
|
|
3914
|
-
|
|
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
|
-
|
|
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, {
|
|
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('
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
},
|
|
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 (
|
|
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
|
|
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 (
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
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
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
)
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
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
|
-
|
|
5706
|
+
takeNativeDown()
|
|
5607
5707
|
tabCtx.effect(() => {
|
|
5608
5708
|
const offTab = betterSidebar.registerTab({
|
|
5609
5709
|
id: TAB_ID,
|