dsh-ide-git 0.21.0 → 0.21.2
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 +107 -45
- package/src/index.js +64 -2
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-ide-git",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
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.21.
|
|
3
|
+
"version": "0.21.2",
|
|
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
|
@@ -173,7 +173,7 @@ window.__ModuleLoader__.load({
|
|
|
173
173
|
'action.mergeIntoCurrent': '合并到当前分支',
|
|
174
174
|
'action.rebaseCurrentOnto': '将当前分支变基到此',
|
|
175
175
|
'action.compare': '与当前分支比较',
|
|
176
|
-
'action.update': '
|
|
176
|
+
'action.update': '拉取',
|
|
177
177
|
'action.push': '推送',
|
|
178
178
|
'action.newTag': '新建标签...',
|
|
179
179
|
'action.cherryPick': '优选(Cherry-Pick)',
|
|
@@ -515,7 +515,7 @@ window.__ModuleLoader__.load({
|
|
|
515
515
|
'action.mergeIntoCurrent': 'Merge into current branch',
|
|
516
516
|
'action.rebaseCurrentOnto': 'Rebase current branch onto this',
|
|
517
517
|
'action.compare': 'Compare with current branch',
|
|
518
|
-
'action.update': '
|
|
518
|
+
'action.update': 'Pull',
|
|
519
519
|
'action.push': 'Push',
|
|
520
520
|
'action.newTag': 'New tag...',
|
|
521
521
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -865,7 +865,7 @@ window.__ModuleLoader__.load({
|
|
|
865
865
|
'action.mergeIntoCurrent': '合併到當前分支',
|
|
866
866
|
'action.rebaseCurrentOnto': '將當前分支變基到此',
|
|
867
867
|
'action.compare': '同當前分支比較',
|
|
868
|
-
|
|
868
|
+
'action.update': '拉取',
|
|
869
869
|
'action.push': '推送',
|
|
870
870
|
'action.newTag': '新建標籤...',
|
|
871
871
|
'action.cherryPick': '揀選',
|
|
@@ -1207,7 +1207,7 @@ window.__ModuleLoader__.load({
|
|
|
1207
1207
|
'action.mergeIntoCurrent': '合併到目前分支',
|
|
1208
1208
|
'action.rebaseCurrentOnto': '將目前分支變基到此',
|
|
1209
1209
|
'action.compare': '與目前分支比較',
|
|
1210
|
-
|
|
1210
|
+
'action.update': '拉取',
|
|
1211
1211
|
'action.push': '推送',
|
|
1212
1212
|
'action.newTag': '新建標籤...',
|
|
1213
1213
|
'action.cherryPick': '揀選',
|
|
@@ -1549,7 +1549,7 @@ window.__ModuleLoader__.load({
|
|
|
1549
1549
|
'action.mergeIntoCurrent': '合併到當前分支',
|
|
1550
1550
|
'action.rebaseCurrentOnto': '將當前分支變基到此',
|
|
1551
1551
|
'action.compare': '同當前分支比較',
|
|
1552
|
-
|
|
1552
|
+
'action.update': '拉取',
|
|
1553
1553
|
'action.push': '推送',
|
|
1554
1554
|
'action.newTag': '新建標籤...',
|
|
1555
1555
|
'action.cherryPick': '揀選',
|
|
@@ -1891,7 +1891,7 @@ window.__ModuleLoader__.load({
|
|
|
1891
1891
|
'action.mergeIntoCurrent': '現在のブランチにマージ',
|
|
1892
1892
|
'action.rebaseCurrentOnto': '現在のブランチをここにリベース',
|
|
1893
1893
|
'action.compare': '現在のブランチと比較',
|
|
1894
|
-
|
|
1894
|
+
'action.update': 'プル',
|
|
1895
1895
|
'action.push': 'プッシュ',
|
|
1896
1896
|
'action.newTag': '新しいタグ...',
|
|
1897
1897
|
'action.cherryPick': 'チェリーピック',
|
|
@@ -2233,7 +2233,7 @@ window.__ModuleLoader__.load({
|
|
|
2233
2233
|
'action.mergeIntoCurrent': '현재 브랜치로 병합',
|
|
2234
2234
|
'action.rebaseCurrentOnto': '현재 브랜치를 여기로 리베이스',
|
|
2235
2235
|
'action.compare': '현재 브랜치와 비교',
|
|
2236
|
-
|
|
2236
|
+
'action.update': '풀',
|
|
2237
2237
|
'action.push': '푸시',
|
|
2238
2238
|
'action.newTag': '새 태그...',
|
|
2239
2239
|
'action.cherryPick': '체리픽',
|
|
@@ -2575,7 +2575,7 @@ window.__ModuleLoader__.load({
|
|
|
2575
2575
|
'action.mergeIntoCurrent': 'In aktuellen Branch mergen',
|
|
2576
2576
|
'action.rebaseCurrentOnto': 'Aktuellen Branch hierauf rebasen',
|
|
2577
2577
|
'action.compare': 'Mit aktuellem Branch vergleichen',
|
|
2578
|
-
|
|
2578
|
+
'action.update': 'Pull',
|
|
2579
2579
|
'action.push': 'Push',
|
|
2580
2580
|
'action.newTag': 'Neuer Tag...',
|
|
2581
2581
|
'action.cherryPick': 'Cherry-Pick',
|
|
@@ -2917,7 +2917,7 @@ window.__ModuleLoader__.load({
|
|
|
2917
2917
|
'action.mergeIntoCurrent': 'Fusionner dans la branche courante',
|
|
2918
2918
|
'action.rebaseCurrentOnto': 'Rebaser la branche courante sur celle-ci',
|
|
2919
2919
|
'action.compare': 'Comparer avec la branche courante',
|
|
2920
|
-
|
|
2920
|
+
'action.update': 'Tirer',
|
|
2921
2921
|
'action.push': 'Pousser',
|
|
2922
2922
|
'action.newTag': 'Nouvelle étiquette...',
|
|
2923
2923
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -3259,7 +3259,7 @@ window.__ModuleLoader__.load({
|
|
|
3259
3259
|
'action.mergeIntoCurrent': 'Влить в текущую ветку',
|
|
3260
3260
|
'action.rebaseCurrentOnto': 'Перебазировать текущую ветку сюда',
|
|
3261
3261
|
'action.compare': 'Сравнить с текущей веткой',
|
|
3262
|
-
|
|
3262
|
+
'action.update': 'Забрать',
|
|
3263
3263
|
'action.push': 'Отправить',
|
|
3264
3264
|
'action.newTag': 'Новый тег...',
|
|
3265
3265
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -3601,7 +3601,7 @@ window.__ModuleLoader__.load({
|
|
|
3601
3601
|
'action.mergeIntoCurrent': 'Mesclar na branch atual',
|
|
3602
3602
|
'action.rebaseCurrentOnto': 'Rebasear a branch atual sobre esta',
|
|
3603
3603
|
'action.compare': 'Comparar com a branch atual',
|
|
3604
|
-
|
|
3604
|
+
'action.update': 'Puxar',
|
|
3605
3605
|
'action.push': 'Enviar',
|
|
3606
3606
|
'action.newTag': 'Nova tag...',
|
|
3607
3607
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -3943,7 +3943,7 @@ window.__ModuleLoader__.load({
|
|
|
3943
3943
|
'action.mergeIntoCurrent': 'Unisci nel branch corrente',
|
|
3944
3944
|
'action.rebaseCurrentOnto': 'Rebase del branch corrente su questo',
|
|
3945
3945
|
'action.compare': 'Confronta con il branch corrente',
|
|
3946
|
-
|
|
3946
|
+
'action.update': 'Pull',
|
|
3947
3947
|
'action.push': 'Push',
|
|
3948
3948
|
'action.newTag': 'Nuovo tag...',
|
|
3949
3949
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -4285,7 +4285,7 @@ window.__ModuleLoader__.load({
|
|
|
4285
4285
|
'action.mergeIntoCurrent': 'Samenvoegen in huidige branch',
|
|
4286
4286
|
'action.rebaseCurrentOnto': 'Huidige branch rebasen op deze',
|
|
4287
4287
|
'action.compare': 'Vergelijken met huidige branch',
|
|
4288
|
-
|
|
4288
|
+
'action.update': 'Pull',
|
|
4289
4289
|
'action.push': 'Pushen',
|
|
4290
4290
|
'action.newTag': 'Nieuwe tag...',
|
|
4291
4291
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -4627,7 +4627,7 @@ window.__ModuleLoader__.load({
|
|
|
4627
4627
|
'action.mergeIntoCurrent': 'Scal z bieżącą gałęzią',
|
|
4628
4628
|
'action.rebaseCurrentOnto': 'Zrebase’uj bieżącą gałąź na tę',
|
|
4629
4629
|
'action.compare': 'Porównaj z bieżącą gałęzią',
|
|
4630
|
-
|
|
4630
|
+
'action.update': 'Ściągnij',
|
|
4631
4631
|
'action.push': 'Wypchnij',
|
|
4632
4632
|
'action.newTag': 'Nowy tag...',
|
|
4633
4633
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -4969,7 +4969,7 @@ window.__ModuleLoader__.load({
|
|
|
4969
4969
|
'action.mergeIntoCurrent': 'Sammanfoga in i aktuell gren',
|
|
4970
4970
|
'action.rebaseCurrentOnto': 'Rebasa aktuell gren mot denna',
|
|
4971
4971
|
'action.compare': 'Jämför med aktuell gren',
|
|
4972
|
-
|
|
4972
|
+
'action.update': 'Pull',
|
|
4973
4973
|
'action.push': 'Pusha',
|
|
4974
4974
|
'action.newTag': 'Ny tagg...',
|
|
4975
4975
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -5311,7 +5311,7 @@ window.__ModuleLoader__.load({
|
|
|
5311
5311
|
'action.mergeIntoCurrent': 'Geçerli dala birleştir',
|
|
5312
5312
|
'action.rebaseCurrentOnto': 'Geçerli dalı bunun üzerine rebase et',
|
|
5313
5313
|
'action.compare': 'Geçerli dalla karşılaştır',
|
|
5314
|
-
|
|
5314
|
+
'action.update': 'Pull',
|
|
5315
5315
|
'action.push': 'Gönder',
|
|
5316
5316
|
'action.newTag': 'Yeni etiket...',
|
|
5317
5317
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -5653,7 +5653,7 @@ window.__ModuleLoader__.load({
|
|
|
5653
5653
|
'action.mergeIntoCurrent': 'Gabungkan ke branch saat ini',
|
|
5654
5654
|
'action.rebaseCurrentOnto': 'Rebase branch saat ini ke ini',
|
|
5655
5655
|
'action.compare': 'Bandingkan dengan branch saat ini',
|
|
5656
|
-
|
|
5656
|
+
'action.update': 'Pull',
|
|
5657
5657
|
'action.push': 'Push',
|
|
5658
5658
|
'action.newTag': 'Tag baru...',
|
|
5659
5659
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -5995,7 +5995,7 @@ window.__ModuleLoader__.load({
|
|
|
5995
5995
|
'action.mergeIntoCurrent': 'Hợp nhất vào nhánh hiện tại',
|
|
5996
5996
|
'action.rebaseCurrentOnto': 'Rebase nhánh hiện tại lên nhánh này',
|
|
5997
5997
|
'action.compare': 'So sánh với nhánh hiện tại',
|
|
5998
|
-
|
|
5998
|
+
'action.update': 'Pull',
|
|
5999
5999
|
'action.push': 'Push',
|
|
6000
6000
|
'action.newTag': 'Thẻ mới...',
|
|
6001
6001
|
'action.cherryPick': 'Cherry-pick',
|
|
@@ -6337,7 +6337,7 @@ window.__ModuleLoader__.load({
|
|
|
6337
6337
|
'action.mergeIntoCurrent': 'الدمج في الفرع الحالي',
|
|
6338
6338
|
'action.rebaseCurrentOnto': 'إعادة تأسيس الفرع الحالي على هذا',
|
|
6339
6339
|
'action.compare': 'المقارنة مع الفرع الحالي',
|
|
6340
|
-
|
|
6340
|
+
'action.update': 'سحب',
|
|
6341
6341
|
'action.push': 'دفع',
|
|
6342
6342
|
'action.newTag': 'وسم جديد...',
|
|
6343
6343
|
'action.cherryPick': 'انتقاء',
|
|
@@ -6679,7 +6679,7 @@ window.__ModuleLoader__.load({
|
|
|
6679
6679
|
'action.mergeIntoCurrent': 'वर्तमान शाखा में मर्ज करें',
|
|
6680
6680
|
'action.rebaseCurrentOnto': 'वर्तमान शाखा को इस पर रीबेस करें',
|
|
6681
6681
|
'action.compare': 'वर्तमान शाखा से तुलना करें',
|
|
6682
|
-
|
|
6682
|
+
'action.update': 'पुल',
|
|
6683
6683
|
'action.push': 'पुश',
|
|
6684
6684
|
'action.newTag': 'नया टैग...',
|
|
6685
6685
|
'action.cherryPick': 'चेरी-पिक',
|
|
@@ -7021,7 +7021,7 @@ window.__ModuleLoader__.load({
|
|
|
7021
7021
|
'action.mergeIntoCurrent': 'รวมเข้าแบรนช์ปัจจุบัน',
|
|
7022
7022
|
'action.rebaseCurrentOnto': 'รีเบสแบรนช์ปัจจุบันไปที่นี้',
|
|
7023
7023
|
'action.compare': 'เปรียบเทียบกับแบรนช์ปัจจุบัน',
|
|
7024
|
-
|
|
7024
|
+
'action.update': 'Pull',
|
|
7025
7025
|
'action.push': 'พุช',
|
|
7026
7026
|
'action.newTag': 'แท็กใหม่...',
|
|
7027
7027
|
'action.cherryPick': 'เชอร์รีพิก',
|
|
@@ -7425,6 +7425,19 @@ window.__ModuleLoader__.load({
|
|
|
7425
7425
|
function isAuthRejection(status) {
|
|
7426
7426
|
return status === 401 || status === 403
|
|
7427
7427
|
}
|
|
7428
|
+
|
|
7429
|
+
/* Whether a proxy answer looks like OUR route (a JSON object). With no
|
|
7430
|
+
remote-web-ui installed the SPA static fallback serves /remote/<path> as
|
|
7431
|
+
'405 Method Not Allowed' with an EMPTY body (issue #20: on a plain-HTTP
|
|
7432
|
+
LAN page the panel surfaced a bare 'HTTP 405' instead of the real
|
|
7433
|
+
403 refusal). A non-JSON retry therefore proves there is no proxy, and
|
|
7434
|
+
the original answer must be what the caller sees. Cloning leaves the
|
|
7435
|
+
caller's own body untouched. */
|
|
7436
|
+
async function answersJson(response) {
|
|
7437
|
+
let body = null
|
|
7438
|
+
try { body = await response.clone().json() } catch (error) { return false }
|
|
7439
|
+
return body !== null && typeof body === 'object'
|
|
7440
|
+
}
|
|
7428
7441
|
/* ---- end remote channel fallback core ---- */
|
|
7429
7442
|
|
|
7430
7443
|
/* Sticky remote-channel mode: once the proxy has answered, later calls go
|
|
@@ -7446,13 +7459,15 @@ window.__ModuleLoader__.load({
|
|
|
7446
7459
|
} else {
|
|
7447
7460
|
response = await fetch(direct, init)
|
|
7448
7461
|
if (isAuthRejection(response.status) === true) {
|
|
7449
|
-
/* Off the origin's trust path: try the pairing proxy once.
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7462
|
+
/* Off the origin's trust path: try the pairing proxy once. It has to
|
|
7463
|
+
answer like our route (a JSON body) to take over: with no
|
|
7464
|
+
remote-web-ui installed the SPA fallback serves the retry as 405
|
|
7465
|
+
with an empty body, and that non-answer must not outrank the
|
|
7466
|
+
ORIGINAL rejection (issue #20). A proxy answer that is itself an
|
|
7467
|
+
auth rejection is discarded the same way, so a genuine refusal can
|
|
7468
|
+
never be masked. */
|
|
7454
7469
|
const retried = await fetch(remoteFallbackUrl(direct), init)
|
|
7455
|
-
if (isAuthRejection(retried.status) !== true) {
|
|
7470
|
+
if (isAuthRejection(retried.status) !== true && (await answersJson(retried)) === true) {
|
|
7456
7471
|
viaRemoteChannel = true
|
|
7457
7472
|
response = retried
|
|
7458
7473
|
}
|
|
@@ -9373,6 +9388,21 @@ window.__ModuleLoader__.load({
|
|
|
9373
9388
|
try { window.localStorage.setItem(LINEDIFF_KEY, JSON.stringify(prefs)) } catch (error) { void error }
|
|
9374
9389
|
}
|
|
9375
9390
|
|
|
9391
|
+
/* The changes view mode (flat vs by directory) is a habit too, not tab
|
|
9392
|
+
state: one localStorage home read on mount, so moving between
|
|
9393
|
+
repositories or reopening the panel keeps the view the user chose
|
|
9394
|
+
instead of snapping back to flat (v0.21.2). */
|
|
9395
|
+
const CHANGES_GROUP_KEY = 'dsh-ide-git.groupBy.v1'
|
|
9396
|
+
function readChangesGroupBy() {
|
|
9397
|
+
try {
|
|
9398
|
+
return window.localStorage.getItem(CHANGES_GROUP_KEY) === 'dir' ? 'dir' : 'flat'
|
|
9399
|
+
} catch (error) { void error }
|
|
9400
|
+
return 'flat'
|
|
9401
|
+
}
|
|
9402
|
+
function rememberChangesGroupBy(value) {
|
|
9403
|
+
try { window.localStorage.setItem(CHANGES_GROUP_KEY, value === 'dir' ? 'dir' : 'flat') } catch (error) { void error }
|
|
9404
|
+
}
|
|
9405
|
+
|
|
9376
9406
|
/* The seed blocks the official review uses, resolved once. Every consumer
|
|
9377
9407
|
null-checks: a host whose primitives predate an export keeps working
|
|
9378
9408
|
through the hand-drawn fallbacks (labels instead of icons, a plain
|
|
@@ -10793,10 +10823,9 @@ window.__ModuleLoader__.load({
|
|
|
10793
10823
|
},
|
|
10794
10824
|
E('span', { className: 'dig-row-icon dig-tone-' + tone + (starred === true ? ' dig-star-filled' : '') }, E(Icon, { name: glyph, size: 12 })),
|
|
10795
10825
|
E('span', { className: 'dig-row-label' }, props.label === undefined ? entry.name : props.label),
|
|
10796
|
-
//
|
|
10797
|
-
//
|
|
10798
|
-
//
|
|
10799
|
-
kind !== 'remote' && kind !== 'tag' && entry.upstream !== null && entry.upstream !== undefined ? E('span', { className: 'dig-row-upstream' }, '→ ' + entry.upstream) : null,
|
|
10826
|
+
// The inline "→ upstream" target was removed (v0.21.2): it ate the
|
|
10827
|
+
// label's width so long branch names truncated early, and the binding
|
|
10828
|
+
// is still one hover away (the row title keeps "name → upstream").
|
|
10800
10829
|
// A bare 'W' told nobody anything (reported right after the section counts):
|
|
10801
10830
|
// it means this branch is checked out in ANOTHER working tree, which is why
|
|
10802
10831
|
// git refuses to check it out here. An icon plus a spelled-out tooltip says so.
|
|
@@ -12713,7 +12742,14 @@ window.__ModuleLoader__.load({
|
|
|
12713
12742
|
rail action, invariant 14) can drive the same groupBy and the sort
|
|
12714
12743
|
order it introduces — one source of truth, wherever the click came
|
|
12715
12744
|
from. sortKey 'git' keeps git's own status order. */
|
|
12716
|
-
|
|
12745
|
+
/* Both the panel and the ⋯ menu write through this wrapper, so the
|
|
12746
|
+
choice survives a remount (the menu used to be the only writer that
|
|
12747
|
+
did not persist, and a repo/session switch silently reset it). */
|
|
12748
|
+
const [groupBy, setGroupByState] = useState(readChangesGroupBy)
|
|
12749
|
+
const setGroupBy = useCallback((value) => {
|
|
12750
|
+
setGroupByState(value)
|
|
12751
|
+
rememberChangesGroupBy(value)
|
|
12752
|
+
}, [])
|
|
12717
12753
|
const [sortKey, setSortKey] = useState('git')
|
|
12718
12754
|
/* The Git-output drawer (VS Code showOutput): null = closed, an array =
|
|
12719
12755
|
the entries it currently shows. Opened on demand, refreshed by hand or
|
|
@@ -12949,9 +12985,15 @@ window.__ModuleLoader__.load({
|
|
|
12949
12985
|
|
|
12950
12986
|
const run = useCallback(async (method, payload) => {
|
|
12951
12987
|
const result = await guard(() => request(method, Object.assign({}, base, payload)))
|
|
12952
|
-
if (result !== undefined)
|
|
12988
|
+
if (result !== undefined) {
|
|
12989
|
+
await refresh()
|
|
12990
|
+
/* v0.21.2: the composer pill is a separate tree; without this it only
|
|
12991
|
+
learned about an in-panel checkout/commit at the next 30s poll. The
|
|
12992
|
+
panel's own watcher skips source 'panel' (refresh already ran). */
|
|
12993
|
+
if (repoRoot !== null) notifyBranchSwitched(sessionId, repoRoot, 'panel')
|
|
12994
|
+
}
|
|
12953
12995
|
return result
|
|
12954
|
-
}, [base, guard, refresh])
|
|
12996
|
+
}, [base, guard, refresh, sessionId, repoRoot])
|
|
12955
12997
|
|
|
12956
12998
|
const pickRepo = useCallback((path) => {
|
|
12957
12999
|
setRepoRoot(path)
|
|
@@ -12984,8 +13026,9 @@ window.__ModuleLoader__.load({
|
|
|
12984
13026
|
if (sid !== sessionId || typeof root !== 'string' || root === '') return
|
|
12985
13027
|
setRepoRoot((current) => (current === root ? current : pickRepo(root)))
|
|
12986
13028
|
}
|
|
12987
|
-
const onBranch = () => {
|
|
12988
|
-
if (
|
|
13029
|
+
const onBranch = (sid, root, source) => {
|
|
13030
|
+
if (sid !== sessionId || source === 'panel') return
|
|
13031
|
+
if (repoRoot === null || root !== repoRoot) return
|
|
12989
13032
|
void refresh()
|
|
12990
13033
|
}
|
|
12991
13034
|
composerRepoWatchers.add(onRepo)
|
|
@@ -13436,8 +13479,15 @@ window.__ModuleLoader__.load({
|
|
|
13436
13479
|
].filter((item) => item !== null))
|
|
13437
13480
|
return
|
|
13438
13481
|
}
|
|
13482
|
+
const updateItem = { id: 'update', icon: 'fetch', tone: 'cyan', label: t('action.update'), run: () => { void run('fetch', { prune: true }) } }
|
|
13483
|
+
const pushItem = { id: 'push', icon: 'push', tone: 'success', label: t('action.push'), disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'push' }) }
|
|
13439
13484
|
const items = [
|
|
13440
13485
|
{ id: 'checkout', icon: 'checkout', tone: 'accent', label: t('action.checkout'), disabled: entry.head === true || entry.tag === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void checkout(entry) } },
|
|
13486
|
+
// v0.21.2: on a LOCAL row the two high-frequency verbs sit right under
|
|
13487
|
+
// 签出 (2nd/3rd) instead of past 删除; remote/tag rows keep the old
|
|
13488
|
+
// tail placement (their menu is about the ref, not the working tree).
|
|
13489
|
+
isLocal ? updateItem : null,
|
|
13490
|
+
isLocal ? pushItem : null,
|
|
13441
13491
|
isLocal ? { id: 'rebase', icon: 'compare', tone: 'violet', label: t('action.rebaseCurrentOnto'), disabled: entry.head === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void run('rebase', { onto: entry.name }) } } : null,
|
|
13442
13492
|
isLocal ? { id: 'merge', icon: 'compare', tone: 'accent', label: t('action.mergeIntoCurrent'), disabled: entry.head === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void run('merge', { branch: entry.name }) } } : null,
|
|
13443
13493
|
{ id: 'compare', icon: 'filter', tone: 'violet', label: t('action.compare'), disabled: entry.head === true || current === '', reason: reason, run: () => { void compareWith(current, entry.name) } },
|
|
@@ -13479,8 +13529,8 @@ window.__ModuleLoader__.load({
|
|
|
13479
13529
|
},
|
|
13480
13530
|
}),
|
|
13481
13531
|
null,
|
|
13482
|
-
|
|
13483
|
-
|
|
13532
|
+
isLocal ? null : updateItem,
|
|
13533
|
+
isLocal ? null : pushItem,
|
|
13484
13534
|
].filter((item) => item !== null)
|
|
13485
13535
|
openMenuAt(event, items)
|
|
13486
13536
|
}, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked, sendGitSummaryToChat, repoLabel, copyValues])
|
|
@@ -14997,7 +15047,15 @@ window.__ModuleLoader__.load({
|
|
|
14997
15047
|
tone) via the host's body[data-ds-dark-theme] switch. Selection reads
|
|
14998
15048
|
as gray on both themes, wallpaper/frost stays visible, never white. */
|
|
14999
15049
|
'.dig-row-selected{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover)) 58%,var(--dsw-static-neutral-bluish-75,var(--dsw-alias-bg-layer-3)) 42%);color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
|
|
15000
|
-
|
|
15050
|
+
/* v0.21.2: the checked-out row is amber; selecting it must not wash that
|
|
15051
|
+
out to primary — the selection is a frame, not a recolour. */
|
|
15052
|
+
'.dig-row-head.dig-row-selected{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
|
|
15053
|
+
/* v0.21.2 (user): on the dark theme the 58/42 charcoal mix still read as
|
|
15054
|
+
a bright frame over a wallpaper — the selection should be a BLACK frame
|
|
15055
|
+
that the wallpaper shows through, not a lit one. rgba(0,0,0,.55) is
|
|
15056
|
+
opaque enough to hold the amber/primary label, transparent enough to
|
|
15057
|
+
stay in the any-background story. The light theme keeps the host gray. */
|
|
15058
|
+
'body[data-ds-dark-theme] .dig-row-selected{background:rgba(0,0,0,.55)}',
|
|
15001
15059
|
'.dig-row-remote .dig-row-label{color:var(--dsw-alias-label-secondary);font-weight:500}',
|
|
15002
15060
|
'.dig-row-tag .dig-row-label{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-weight:500}',
|
|
15003
15061
|
'.dig-row-head .dig-row-label{font-weight:600}',
|
|
@@ -15005,7 +15063,6 @@ window.__ModuleLoader__.load({
|
|
|
15005
15063
|
'.dig-row-label{overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0;font-weight:600}',
|
|
15006
15064
|
// Inline tracking target (issue #18): flex:none so it never starves the
|
|
15007
15065
|
// label, capped and ellipsised so a long origin/… cannot push the badges.
|
|
15008
|
-
'.dig-row-upstream{flex:none;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}',
|
|
15009
15066
|
'.dig-row-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis}',
|
|
15010
15067
|
'.dig-row-dir{margin-left:auto;opacity:.75}',
|
|
15011
15068
|
'.dig-badge{font-size:10px;padding:0 5px;border-radius:999px;corner-shape:round;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);flex:none}',
|
|
@@ -15455,7 +15512,9 @@ window.__ModuleLoader__.load({
|
|
|
15455
15512
|
right repository without any extra wiring. */
|
|
15456
15513
|
|
|
15457
15514
|
const composerRepoWatchers = new Set() /* (sessionId, repoRoot) */
|
|
15458
|
-
|
|
15515
|
+
/* (sessionId, repoRoot, source) — source is 'composer' | 'panel' so the
|
|
15516
|
+
seat that performed the switch can skip its own echo (v0.21.2). */
|
|
15517
|
+
const composerBranchWatchers = new Set()
|
|
15459
15518
|
|
|
15460
15519
|
function rememberRepoShared(sessionId, repoRoot) {
|
|
15461
15520
|
rememberRepo(sessionId, repoRoot)
|
|
@@ -15464,9 +15523,9 @@ window.__ModuleLoader__.load({
|
|
|
15464
15523
|
}
|
|
15465
15524
|
}
|
|
15466
15525
|
|
|
15467
|
-
function notifyBranchSwitched(sessionId, repoRoot) {
|
|
15526
|
+
function notifyBranchSwitched(sessionId, repoRoot, source) {
|
|
15468
15527
|
for (const watcher of composerBranchWatchers) {
|
|
15469
|
-
try { watcher(sessionId, repoRoot) } catch (error) { void error }
|
|
15528
|
+
try { watcher(sessionId, repoRoot, source) } catch (error) { void error }
|
|
15470
15529
|
}
|
|
15471
15530
|
}
|
|
15472
15531
|
|
|
@@ -15901,7 +15960,10 @@ window.__ModuleLoader__.load({
|
|
|
15901
15960
|
/* A branch switched in the panel (or here) refreshes the summary now
|
|
15902
15961
|
instead of at the next poll tick. */
|
|
15903
15962
|
useEffect(() => {
|
|
15904
|
-
const watcher = (
|
|
15963
|
+
const watcher = (sid, root, source) => {
|
|
15964
|
+
if (sid !== sessionId || source === 'composer') return
|
|
15965
|
+
setTick((value) => value + 1)
|
|
15966
|
+
}
|
|
15905
15967
|
composerBranchWatchers.add(watcher)
|
|
15906
15968
|
return () => { composerBranchWatchers.delete(watcher) }
|
|
15907
15969
|
}, [sessionId])
|
|
@@ -15979,7 +16041,7 @@ window.__ModuleLoader__.load({
|
|
|
15979
16041
|
await request('checkout', { cwd: cwd, repoRoot: repoRoot, branch: name, confirm: true })
|
|
15980
16042
|
setMenu(null)
|
|
15981
16043
|
setTick((value) => value + 1)
|
|
15982
|
-
notifyBranchSwitched(sessionId, repoRoot)
|
|
16044
|
+
notifyBranchSwitched(sessionId, repoRoot, 'composer')
|
|
15983
16045
|
} catch (caught) {
|
|
15984
16046
|
setMenu(null)
|
|
15985
16047
|
setError(caught instanceof Error ? caught.message : String(caught))
|
package/src/index.js
CHANGED
|
@@ -2086,6 +2086,61 @@ function headerOf(req, name) {
|
|
|
2086
2086
|
return value
|
|
2087
2087
|
}
|
|
2088
2088
|
|
|
2089
|
+
/* Normalize an authority ("host[:port]" or an absolute URL) to
|
|
2090
|
+
"hostname:port", filling the scheme default port when it is implicit.
|
|
2091
|
+
Returns '' for anything unparseable (including the "null" opaque origin). */
|
|
2092
|
+
function authorityKey(value, protocol) {
|
|
2093
|
+
const text = String(value === undefined || value === null ? '' : value).trim()
|
|
2094
|
+
if (text === '') return ''
|
|
2095
|
+
let hostname = text
|
|
2096
|
+
let port = ''
|
|
2097
|
+
let scheme = protocol
|
|
2098
|
+
if (text.includes('://') === true) {
|
|
2099
|
+
let parsed = null
|
|
2100
|
+
try { parsed = new URL(text) } catch (error) { parsed = null }
|
|
2101
|
+
if (parsed === null) return ''
|
|
2102
|
+
hostname = parsed.hostname
|
|
2103
|
+
port = parsed.port
|
|
2104
|
+
scheme = parsed.protocol
|
|
2105
|
+
} else {
|
|
2106
|
+
const bracketed = /^\[([^\]]+)\](?::(\d+))?$/.exec(text)
|
|
2107
|
+
if (bracketed !== null) {
|
|
2108
|
+
hostname = bracketed[1]
|
|
2109
|
+
port = bracketed[2] === undefined ? '' : bracketed[2]
|
|
2110
|
+
} else {
|
|
2111
|
+
const plain = /^([^:]+)(?::(\d+))?$/.exec(text)
|
|
2112
|
+
if (plain === null) return ''
|
|
2113
|
+
hostname = plain[1]
|
|
2114
|
+
port = plain[2] === undefined ? '' : plain[2]
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (hostname === '') return ''
|
|
2118
|
+
if (port === '') port = scheme === 'https:' ? '443' : '80'
|
|
2119
|
+
return hostname.toLowerCase() + ':' + port
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/* Same-origin fallback for browsers that send no Sec-Fetch-* headers. A page
|
|
2123
|
+
served over plain HTTP from a non-loopback address is not a secure context,
|
|
2124
|
+
and the fetch-metadata headers are omitted there (issue #20: the git panel
|
|
2125
|
+
answered 403 on a LAN address). Origin and Referer are forbidden headers a
|
|
2126
|
+
page cannot forge — a cross-site request always carries the caller's own
|
|
2127
|
+
origin — so comparing either against the Host header restores the
|
|
2128
|
+
same-origin fence without trusting anything the request body claims. */
|
|
2129
|
+
function sameOriginOf(req, host) {
|
|
2130
|
+
const hostKey = authorityKey(host, 'http:')
|
|
2131
|
+
if (hostKey === '') return false
|
|
2132
|
+
/* A Host header without a port says nothing about the scheme, so either
|
|
2133
|
+
scheme default may be the one the browser used in Origin. */
|
|
2134
|
+
const candidates = new Set([hostKey])
|
|
2135
|
+
if (/:\d+$/.test(String(host).trim()) !== true) candidates.add(authorityKey(host, 'https:'))
|
|
2136
|
+
const matches = (value) => {
|
|
2137
|
+
const key = authorityKey(value, 'http:')
|
|
2138
|
+
return key !== '' && candidates.has(key)
|
|
2139
|
+
}
|
|
2140
|
+
if (matches(headerOf(req, 'origin')) === true) return true
|
|
2141
|
+
return matches(headerOf(req, 'referer'))
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2089
2144
|
/** Loopback authority, or a same-origin browser request (desktop shell / remote UI). */
|
|
2090
2145
|
function isTrusted(req) {
|
|
2091
2146
|
const host = headerOf(req, 'host')
|
|
@@ -2094,7 +2149,8 @@ function isTrusted(req) {
|
|
|
2094
2149
|
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '0.0.0.0') return true
|
|
2095
2150
|
if (/^127\./.test(hostname)) return true
|
|
2096
2151
|
const site = headerOf(req, 'sec-fetch-site')
|
|
2097
|
-
|
|
2152
|
+
if (site === 'same-origin' || site === 'same-site') return true
|
|
2153
|
+
return sameOriginOf(req, host)
|
|
2098
2154
|
}
|
|
2099
2155
|
|
|
2100
2156
|
function sendJson(res, status, body) {
|
|
@@ -2136,7 +2192,13 @@ function readJsonBody(req, limit = 1 << 20) {
|
|
|
2136
2192
|
|
|
2137
2193
|
async function handle(req, res) {
|
|
2138
2194
|
if (!isTrusted(req)) {
|
|
2139
|
-
sendJson(res, 403, {
|
|
2195
|
+
sendJson(res, 403, {
|
|
2196
|
+
ok: false,
|
|
2197
|
+
error: {
|
|
2198
|
+
code: 'forbidden',
|
|
2199
|
+
message: 'forbidden: a non-loopback request must be same-origin (open this host from its own page) or come through the remote pairing proxy',
|
|
2200
|
+
},
|
|
2201
|
+
})
|
|
2140
2202
|
return
|
|
2141
2203
|
}
|
|
2142
2204
|
if (req.method !== 'POST') {
|