source-code-mgmt 1.5.0 → 1.8.0

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.
Files changed (4) hide show
  1. package/README.md +26 -10
  2. package/lib/client.js +206 -132
  3. package/lib/index.js +63 -0
  4. package/package.json +38 -38
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # source-code-mgmt — DSH 源代码管理插件
2
2
 
3
- > 版本:**v1.5.0** | 更新日志见文末「[版本历史](#版本历史)」
3
+ > 版本:**v1.8.0** | 更新日志见文末「[版本历史](#版本历史)」
4
4
 
5
5
  > DSH Web GUI 源代码管理插件:把「环境检查 → SSH 配置 → 代码上传推送」整合进「代码管理」面板,支持 GitHub / Gitee 双平台,一键管理代码仓库。
6
6
 
7
- > 入口位置自适应:**已安装 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时**,「代码管理」作为它侧边栏的一个新 Tab 页面出现(全新侧边栏 Tab);**未安装时**,在 DSH 页面**右上角显示一个浮动按钮**,点击展开右侧栏抽屉。两种形态都复用同一套面板 UI,且**不再占用左栏底部按钮**。
7
+ > 入口位置自适应:**已安装 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时**,「代码管理」作为它侧边栏的一个新 Tab 页面出现(全新侧边栏 Tab);**未安装时**,「代码管理」按钮出现在 DSH 页面**右上角 Session log 旁边**(同一右对齐列表、同款胶囊样式、间距 8px 不挤在一起),点击后打开一个 **dsh-better-sidebar 外观的右侧集成面板**(推挤主内容区)。两种形态都复用同一套面板 UI,且**不再占用左栏底部按钮**。
8
8
 
9
9
  ## 功能
10
10
 
11
11
  集成入口(二选一,自动检测,无需手动切换):
12
12
 
13
13
  - **已安装 dsh-better-sidebar**:「代码管理」注册为它侧边栏的一个**新 Tab 页面**,点击侧边栏 Tab 直接打开面板;
14
- - **未安装 dsh-better-sidebar**:DSH 页面**右上角出现一个「代码管理」浮动按钮**,点击后展开**右侧栏抽屉**(形态类似 dsh-better-sidebar 的右栏),内容放同一面板。
14
+ - **未安装 dsh-better-sidebar**:「代码管理」按钮出现在 DSH 页面**右上角 Session log 旁边的右对齐列表**里(通过 DSH 的 `conversation.session.header.utilities` 槽位注册,与 Session log 同款胶囊样式、间距 8px 不挤在一起),点击后打开一个 **dsh-better-sidebar 外观的右侧集成面板**(内容放同一面板),并把主内容区往左推挤。
15
15
 
16
16
  > 检测只是激活时一次内存读取(`ctx.get('betterSidebar')`),零 I/O、零网络,不影响 DSH 启动速度;两种形态间自动切换,**不再占用左栏底部的按钮**。
17
17
 
@@ -54,8 +54,8 @@
54
54
  - Gitee:用令牌调 Gitee OpenAPI `POST /user/repos` 建仓,再设置 SSH 远程 `git@gitee.com:<owner>/<name>.git` 并 `git push`(走 ② 已配的 SSH 密钥)
55
55
  - **>100MB 文件处理**:自动识别超过 100MB 单文件限制的文件——文件在一级子目录内则**忽略整个一级目录**(该文件夹为一整体),根目录独立文件则**忽略单个文件**;已存在于 `.gitignore` 的不重复添加,并显示「未上传原因」
56
56
 
57
- ### 预加载优化
58
- DSH 打开时自动预取环境/SSH/工作区/仓库状态,点开面板**直接秒显**,无需重新加载。
57
+ ### 数据加载时机(打开时联网、显示刷新中)
58
+ DSH 打开时**不联网同步仓库**,只预取静态的环境/SSH/工作区列表。**打开插件、切换工作区、刷新状态、以及推送/拉取/暂存/提交等操作后**,都会联网获取对应工作区的最新仓库状态,并显示「⟳ 刷新中…」提示——避免打开/重开/切换时显示可能过期的旧数据(如旧的「无改动」)。关闭面板再打开也会重新同步,不会停留在旧状态。
59
59
 
60
60
  ## 安装
61
61
 
@@ -93,7 +93,7 @@ dsh web
93
93
  name: 'source-code-mgmt'
94
94
  ```
95
95
 
96
- 保存后**完全重启 dsh web**(不是刷新页面,而是要停掉旧进程后重新启动),然后浏览器 **F5 刷新**,「代码管理」入口即出现(已装 dsh-better-sidebar 时为侧边栏 Tab,未装时为右上角浮动按钮)。
96
+ 保存后**完全重启 dsh web**(不是刷新页面,而是要停掉旧进程后重新启动),然后浏览器 **F5 刷新**,「代码管理」入口即出现(已装 dsh-better-sidebar 时为侧边栏 Tab,未装时为右上角 Session log 旁的「代码管理」按钮 + 右侧集成面板)。
97
97
 
98
98
  > 用命令直接追加(幂等,已存在则跳过)——PowerShell:
99
99
  > ```powershell
@@ -165,7 +165,7 @@ dsh web
165
165
  1. **依赖已写入**:`~/.dsh/profiles/web/package.json` 的 `dependencies` 里应有 `source-code-mgmt`。
166
166
  2. **符号链接已建立(`link:` 方式)**:`~/.dsh/profiles/web/node_modules/source-code-mgmt` 指向源码目录(Windows 显示为 Junction)。
167
167
  3. **激活条目已添加**:`~/.dsh/profiles/web/cordis.patch.yml` 里有 `source-code-mgmt` 的 insert 条目。
168
- 4. **重启后入口可见**:已装 dsh-better-sidebar 时侧边栏出现「代码管理」Tab;未装时右上角出现「代码管理」浮动按钮。
168
+ 4. **重启后入口可见**:已装 dsh-better-sidebar 时侧边栏出现「代码管理」Tab;未装时右上角 Session log 旁出现「代码管理」按钮,点击展开右侧集成面板。
169
169
 
170
170
  ### 常见排障
171
171
 
@@ -178,8 +178,8 @@ dsh web
178
178
  ## 使用步骤
179
179
 
180
180
  1. 重启 dsh web 并刷新浏览器
181
- 2. 点击「**代码管理**」入口(已装 dsh-better-sidebar 时点侧边栏 Tab,未装时点右上角浮动按钮)
182
- 3. 面板打开(秒显预加载数据)
181
+ 2. 点击「**代码管理**」入口(已装 dsh-better-sidebar 时点侧边栏 Tab,未装时点右上角 Session log 旁的「代码管理」按钮展开右侧集成面板)
182
+ 3. 面板打开(联网获取当前工作区最新状态,显示「⟳ 刷新中…」,拉取完成即显示内容)
183
183
  4. ①确认 Git / GitHub CLI 已安装 → ②生成密钥并测试连接 → ③选择工作区后推送或新建仓库
184
184
 
185
185
  ## 后端 API 路由
@@ -250,7 +250,23 @@ pnpm add link:$(pwd)
250
250
 
251
251
  ## 版本历史
252
252
 
253
- ### v1.5.0(当前)
253
+ ### v1.8.0(当前)
254
+ 本次更新:
255
+
256
+ - **新增「推送暂存」按钮**(③面板「同步」行):当**本地领先有提交**或**有已暂存的改动**时,「同步」行「本地领先 N 提交」后面出现「**推送暂存**」按钮——点击后**只把已暂存的内容**用**你填写的提交信息**(没填则自动生成 `chore: update <文件夹名>`)提交,然后**推送到远程**;不会像「推送更改」那样自动暂存所有未暂存的改动。推送成功后自动刷新状态并显示「⟳ 刷新中…」,界面显示本次提交的信息与 hash。host 端新增 `POST /push-staged` 路由(`pushStagedFlow`,沿用本插件 `run()`/GIT_SSH)
257
+
258
+ ### v1.7.0(历史)
259
+ 本次更新:
260
+
261
+ - **数据加载时机重做:打开时联网、显示「刷新中」**:此前打开 DSH 会预取所有工作区的仓库状态并缓存,导致打开/重开/切换面板时命中旧缓存,显示可能过期的数据(如旧的「无改动」)。现在改为——**打开 DSH 时不联网同步仓库**(只预取静态的 env/SSH/工作区列表);**打开插件、切换工作区、自定义目录点击、刷新状态、以及推送/拉取/暂存/取消暂存/提交/对齐/初始化等操作后**,一律**联网获取对应工作区的最新状态**,并显示「⟳ 刷新中…」提示(刷新期间按钮变「刷新中…」、状态区显示同步中)。关闭面板再打开也会重新同步,不再停留在旧状态。删除了预取所有工作区的 `syncAllRepos` 与「命中缓存秒显」分支
262
+
263
+ ### v1.6.0(历史)
264
+ 本次更新:
265
+
266
+ - **未安装 dsh-better-sidebar 时的入口改为「右上角 Session log 旁 + 右侧集成面板」**:原「右上角浮动按钮 + 右侧抽屉」改为——「代码管理」按钮经 DSH 的 `conversation.session.header.utilities` 槽位注册,**出现在右上角 Session log 旁边的右对齐列表**里(与 Session log 同款胶囊样式、间距 8px 不挤在一起);点击后在**右侧展开一个 dsh-better-sidebar 外观的集成面板**(复用原话术:环境检查 / SSH / 代码管理三步),并把主内容区 `#root` **往左推挤**(margin-right + width calc)。slots 服务不可用时降级为右上角浮动按钮 + 右侧面板。改动仅限 `lib/client.js`(浏览器端),刷新页面即生效,host 端 `/api` 路由未动
267
+ - **已安装 dsh-better-sidebar 的形态不变**:仍注册为它侧边栏的「代码管理」Tab;安装/未安装两形态依旧自动检测切换
268
+
269
+ ### v1.5.0(历史)
254
270
  本次更新:
255
271
 
256
272
  - **① 环境检查缺工具一键安装**:某个工具(git / gh / ssh)未检测到时,该行显示「❌ 未安装」+「复制安装命令」+「**安装**」按钮——「安装」由 host 自动选包管理器执行(Windows winget / 内置功能、macOS brew、Linux apt/dnf/pacman),安装后自动重新检测;host 新增 `POST /install-tool` 路由(best-effort,回传执行命令与输出)
package/lib/client.js CHANGED
@@ -7,9 +7,13 @@
7
7
  *
8
8
  * - branch A: dsh-better-sidebar installed → register「代码管理」as a new
9
9
  * sidebar Tab page through `ctx.betterSidebar.registerTab`.
10
- * - branch B: not installed → render a floating button pinned to the top
11
- * right; clicking expands a right-side drawer (like better-sidebar's
12
- * right rail) holding the existing panel.
10
+ * - branch B: not installed → register a「代码管理」button into the DSH
11
+ * `conversation.session.header.utilities` slot (the right-aligned list that
12
+ * holds "Session log"), so it sits beside "Session log" with the same pill
13
+ * look and 8px gap; clicking opens a RIGHT-side integrated panel (pushing
14
+ * #root left, dsh-better-sidebar style) holding the existing panel.
15
+ * If the `slots` service is unavailable, it falls back to a floating
16
+ * top-right button opening the same right-side panel.
13
17
  *
14
18
  * The old left-rail bottom button (`sidebar.footer.action` slot) is removed.
15
19
  * Either way clicking the entry opens a "源代码管理" panel with:
@@ -71,14 +75,15 @@ window.__ModuleLoader__.load({
71
75
  }
72
76
 
73
77
  // ---------- preload cache ----------
74
- // DSH 打开(插件激活)时就预先检测并缓存,点开「代码管理」面板直接秒显,
75
- // 不再每次点击重新加载。
78
+ // 只在打开插件时按需拉取仓库状态;DSH 打开(插件激活)时只预取静态的 env / ssh /
79
+ // workspaces / 默认工作区,绝不联网同步仓库——避免打开 DSH 就 fetch、并把可能过期的
80
+ // 数据缓存起来,导致打开/重开面板时显示旧状态(如旧的「无改动」)。
76
81
  const cache = {
77
82
  env: null,
78
83
  ssh: null,
79
84
  defDir: null,
80
85
  repo: null,
81
- // 各工作区的 full 同步结果(dir -> repo 状态),打开 DSH 时预取,切换工作区可秒显。
86
+ // 各工作区的最近一次结果(dir -> repo 状态),仅作展示历史,不再用于「秒显无刷新」。
82
87
  reposByDir: {},
83
88
  // 单文件改动 diff 缓存("dir\u0000path" -> diff 文本),点击「查看」秒出。
84
89
  diffs: {},
@@ -122,7 +127,8 @@ window.__ModuleLoader__.load({
122
127
  await Promise.all(workers)
123
128
  }
124
129
 
125
- /** 预取 env / ssh / default-dir / repo / workspaces,结果写入 cache。可并发安全。 */
130
+ /** 预取 env / ssh / default-dir / workspaces(仅静态资源),结果写入 cache。可并发安全。
131
+ * 注意:仓库状态不在此预取(需要用户打开面板/切换工作区时才按需联网获取,并显示刷新中)。 */
126
132
  function preload() {
127
133
  if (!preloadPromise) {
128
134
  preloadPromise = (async () => {
@@ -144,19 +150,6 @@ window.__ModuleLoader__.load({
144
150
  if (def && def.dir) {
145
151
  cache.defDir = def.dir;
146
152
  }
147
- // 打开 DSH 时即对所有工作区做一次完整同步(full=1 会 git fetch 联网),
148
- // 这样点开「代码管理」或切换任意工作区都直接显示已同步的最新状态,无需再等。
149
- // 用并发上限 5 控制,避免一次性对大量仓库同时打满网络。
150
- const dirs = Array.from(new Set([
151
- ...(cache.workspaces || []),
152
- ...(cache.customDirs || []),
153
- ...(def && def.dir ? [def.dir] : []),
154
- ].filter(Boolean)))
155
- await syncAllRepos(dirs, 5)
156
- // 默认工作区的同步结果作为面板首次秒显内容。
157
- if (def && def.dir && cache.reposByDir[def.dir]) {
158
- cache.repo = cache.reposByDir[def.dir];
159
- }
160
153
  cache.ready = true;
161
154
  } catch {
162
155
  /* 保持部分缓存 */
@@ -166,23 +159,6 @@ window.__ModuleLoader__.load({
166
159
  return preloadPromise;
167
160
  }
168
161
 
169
- /** 并发上限 N 地对所有目录做 full 同步,结果写入 cache.reposByDir[dir]。 */
170
- async function syncAllRepos(dirs, limit) {
171
- let i = 0
172
- const worker = async () => {
173
- while (i < dirs.length) {
174
- const d = dirs[i++]
175
- try {
176
- const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&full=1").catch(() => null)
177
- if (r) cache.reposByDir[d] = r
178
- } catch { /* 单个失败不影响其他 */ }
179
- }
180
- }
181
- const workers = []
182
- const n = Math.max(1, Math.min(limit || 5, dirs.length || 1))
183
- for (let k = 0; k < n; k++) workers.push(worker())
184
- await Promise.all(workers)
185
- }
186
162
  /** 忽略缓存强制重新拉取某个资源并更新 cache。 */
187
163
  async function refreshCache(key) {
188
164
  if (key === "env") cache.env = await jget("/env").catch(() => cache.env);
@@ -448,6 +424,8 @@ window.__ModuleLoader__.load({
448
424
  const [dirDraft, setDirDraft] = useState(() => cache.defDir ?? "");
449
425
  const [repo, setRepo] = useState(() => cache.repo);
450
426
  const [busy, setBusy] = useState(false);
427
+ // 「推送暂存」进行中标记:把已暂存的内容用填写的(或自动生成的)信息提交后推送。
428
+ const [pushStagedBusy, setPushStagedBusy] = useState(false);
451
429
  // 刷新状态(联网同步)进行中的提示状态,避免刷新时界面闪成「未加载文件夹」。
452
430
  const [refreshing, setRefreshing] = useState(false);
453
431
  // 切换平台(② 里 GitHub/Gitee)时,③ 需要重新拉取对应平台的仓库状态;此标记在拉取期间为真,
@@ -526,28 +504,15 @@ window.__ModuleLoader__.load({
526
504
  // 结果/错误提示,造成「点了按钮没有任何反馈」。需要清空 err 的调用方
527
505
  // (切换目录 / 切换平台 / 刷新状态)自行清空。
528
506
  // full=true 时才让 host 端执行 git fetch 联网同步(用于「刷新状态」及
529
- // 推送/拉取/对齐等操作后的刷新);默认 false(快速模式):跳过 fetch,
530
- // 秒出本地状态。full 模式自动显示「刷新中…」提示,提示只在最新一次调用归来时关闭。
507
+ // 推送/拉取/对齐等操作后的刷新);full 模式自动显示「刷新中…」提示,
508
+ // 提示只在最新一次调用归来时关闭。**始终走网络获取最新仓库状态,绝不命中
509
+ // 缓存秒显**——否则打开/重开/切换工作区会显示旧数据(如旧的「无改动」)。
531
510
  const doFull = full === true;
532
511
  const mySeq = ++loadSeq.current;
533
512
  if (doFull) setRefreshing(true);
534
513
  const dirty = !keepRepo;
535
514
  if (dirty) setRepo(null);
536
- // 快速模式且预取已同步过该目录:直接秒显缓存,不再等网络(打开 DSH 时已全部 full 同步)。
537
- // 注意:缓存是按目录存的,但内容随平台(github/gitee)不同。只有缓存的 provider 与
538
- // 当前平台一致才可秒显;否则跳过缓存,走网络重新拉取对应平台的仓库状态,避免切换
539
- // 平台后仍显示上一个平台的检测内容。
540
515
  const curProv = provRef.current || "github";
541
- if (!doFull && cache.reposByDir[d] && cache.reposByDir[d].provider === curProv) {
542
- const cached = cache.reposByDir[d];
543
- if (mySeq === loadSeq.current) {
544
- setRepo(cached);
545
- if (cached.dir) { setDirDraft(cached.dir); setDir(cached.dir); }
546
- syncVisibility(cached);
547
- if (!hasBetterSidebar) void prefetchDiffs(cached.dir, cached.changedFiles, 3);
548
- }
549
- return;
550
- }
551
516
  try {
552
517
  const p = curProv;
553
518
  const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&provider=" + encodeURIComponent(p) + (doFull ? "&full=1" : ""));
@@ -565,10 +530,8 @@ window.__ModuleLoader__.load({
565
530
  setDir(r.dir);
566
531
  cache.defDir = r.dir;
567
532
  }
568
- // 缓存最新 repo 状态
533
+ // 记录最新 repo 状态(仅作展示历史,绝不再用于「无刷新秒显」)。
569
534
  cache.repo = r;
570
- // 按目录缓存(含 provider),下次同一平台可用快速缓存秒显;切平台时因 provider
571
- // 不匹配会跳过缓存重新拉取,避免显示上个平台的检测内容。
572
535
  if (r && r.dir) cache.reposByDir[r.dir] = r;
573
536
  // 默认可见性:优先当前仓库实际状态;无远程(即将新建的仓库)默认「公开」。
574
537
  syncVisibility(r);
@@ -585,9 +548,9 @@ window.__ModuleLoader__.load({
585
548
  }, []);
586
549
 
587
550
  useEffect(() => {
588
- // 首次加载:优先用 DSH 打开时已 full 同步好的 cache.repo 秒显;
589
- // 无论预加载是否已在此时完成,都走 preload()(幂等),在 .then 里
590
- // 直接用预取结果,避免「先显示(未加载文件夹)空白再等一会儿」。
551
+ // 首次加载:只预取静态的 env/ssh/workspaces/默认目录;仓库状态一律在面板打开后
552
+ // 通过 loadRepo(..., true) 联网获取最新(并显示「刷新中…」),绝不命中可能过期的
553
+ // 缓存——这样关闭面板再打开、或切换工作区,都会重新同步,不会停留在旧的「无改动」。
591
554
  if (cache.workspaces && cache.workspaces.length) setWorkspaces(cache.workspaces);
592
555
  if (cache.customDirs && cache.customDirs.length) setCustomDirs(cache.customDirs);
593
556
  void preload().then(() => {
@@ -598,15 +561,8 @@ window.__ModuleLoader__.load({
598
561
  || dir || "";
599
562
  if (!d) return;
600
563
  setDir(d); setDirDraft(d);
601
- if (cache.repo) {
602
- // 预加载已做 full 同步,直接秒显,不再发重复请求。
603
- setRepo(cache.repo);
604
- syncVisibility(cache.repo);
605
- if (!hasBetterSidebar) void prefetchDiffs(cache.repo.dir, cache.repo.changedFiles, 3);
606
- } else {
607
- // 兜底:预加载未拿到(如请求失败),发一次 full 同步刷新。
608
- loadRepo(d, true, true);
609
- }
564
+ // 始终 full 同步(联网 fetch + 显示刷新中),获取当前工作区的最新状态。
565
+ loadRepo(d, true, true);
610
566
  });
611
567
  // eslint-disable-next-line react-hooks/exhaustive-deps
612
568
  }, []);
@@ -632,7 +588,7 @@ window.__ModuleLoader__.load({
632
588
  setGiteeToken("");
633
589
  setGiteeTokenMsg("已保存 Gitee 令牌(账号:" + (r.owner || "?") + ")");
634
590
  // 令牌就绪后刷新仓库,让同名检测等按 Gitee 生效
635
- loadRepo(dir || cache.defDir || "", true);
591
+ loadRepo(dir || cache.defDir || "", true, true);
636
592
  } else {
637
593
  setGiteeConfigured(false);
638
594
  setGiteeTokenErr(r.error || "保存令牌失败");
@@ -649,7 +605,7 @@ window.__ModuleLoader__.load({
649
605
  setGiteeOwnerName("");
650
606
  setGiteeToken("");
651
607
  setGiteeTokenMsg(r.ok ? "已清除 Gitee 令牌" : "清除失败");
652
- loadRepo(dir || cache.defDir || "", true);
608
+ loadRepo(dir || cache.defDir || "", true, true);
653
609
  } catch (e) { setGiteeTokenErr("清除令牌失败:" + e); }
654
610
  finally { setGiteeTokenBusy(false); }
655
611
  }, [dir, loadRepo]);
@@ -662,7 +618,7 @@ window.__ModuleLoader__.load({
662
618
  if (dir || cache.defDir) {
663
619
  // 显示「切换平台,正在重新检测…」提示,避免切换后旧内容停留几秒看起来像没变化。
664
620
  setSwitching(true);
665
- loadRepo(dir || cache.defDir || "", true).finally(() => setSwitching(false));
621
+ loadRepo(dir || cache.defDir || "", true, true).finally(() => setSwitching(false));
666
622
  }
667
623
  // eslint-disable-next-line react-hooks/exhaustive-deps
668
624
  }, [prov]);
@@ -796,7 +752,7 @@ window.__ModuleLoader__.load({
796
752
  // 切换到新目录,清空上一个目录的操作结果/提示。
797
753
  setResult(null); setMsg(null); setErr(null);
798
754
  setPickOpen(false);
799
- loadRepo(r.dir);
755
+ loadRepo(r.dir, true, true);
800
756
  } else {
801
757
  setPickErr(r.error || "添加目录失败");
802
758
  }
@@ -822,7 +778,7 @@ window.__ModuleLoader__.load({
822
778
  if (dir === path) {
823
779
  const next = (r.workspaces && r.workspaces[0]) || "";
824
780
  setDir(next); setDirDraft(next);
825
- if (next) loadRepo(next); else setRepo(null);
781
+ if (next) loadRepo(next, true, true); else setRepo(null);
826
782
  }
827
783
  } else {
828
784
  setErr(r.error || "删除失败");
@@ -840,7 +796,7 @@ window.__ModuleLoader__.load({
840
796
  setResult(r);
841
797
  if (r.ok) setMsg("已强制对齐到远程分支");
842
798
  else setErr(r.error || "强制对齐失败");
843
- void loadRepo(dir);
799
+ void loadRepo(dir, true, true);
844
800
  } catch (e) { setErr("强制对齐失败:" + e); }
845
801
  finally { setBusy(false); }
846
802
  }, [dir, loadRepo]);
@@ -853,7 +809,7 @@ window.__ModuleLoader__.load({
853
809
  setResult(r);
854
810
  if (r.ok) setMsg(r.alreadyRepo ? "已是 git 仓库" : "已创建 git 仓库,请自行拉取或推送");
855
811
  else setErr(r.error || "创建 git 失败");
856
- void loadRepo(dir);
812
+ void loadRepo(dir, true, true);
857
813
  } catch (e) { setErr("创建 git 失败:" + e); }
858
814
  finally { setBusy(false); }
859
815
  }, [dir, loadRepo]);
@@ -869,7 +825,7 @@ window.__ModuleLoader__.load({
869
825
  // 暂存会改变文件的 staged 状态:清掉该目录的缓存快照,强制走网络路径
870
826
  // 重新拉取最新 repo(含最新「已暂存/未暂存」标记),并清 diff 展开状态。
871
827
  if (dir) delete cache.reposByDir[dir];
872
- loadRepo(dir, true);
828
+ loadRepo(dir, true, true);
873
829
  } else setErr(r.error || "暂存失败");
874
830
  } catch (e) { setErr("暂存失败:" + e); }
875
831
  finally { setStageBusy(false); }
@@ -882,7 +838,7 @@ window.__ModuleLoader__.load({
882
838
  const r = await jpost("/unstage", { dir: dir || undefined, path: path || undefined });
883
839
  if (r.ok) {
884
840
  if (dir) delete cache.reposByDir[dir];
885
- loadRepo(dir, true);
841
+ loadRepo(dir, true, true);
886
842
  } else setErr(r.error || "取消暂存失败");
887
843
  } catch (e) { setErr("取消暂存失败:" + e); }
888
844
  finally { setStageBusy(false); }
@@ -905,6 +861,26 @@ window.__ModuleLoader__.load({
905
861
  finally { setBusy(false); }
906
862
  }, [dir, repo, commitMsg, repoName, loadRepo]);
907
863
 
864
+ // 「推送暂存」:把已暂存的内容用填写的(或自动生成的)信息提交后推送远程。
865
+ // 与「推送更改」不同:它只提交暂存区、沿用自定义信息,不自动 add 全部改动。
866
+ const doPushStaged = useCallback(async () => {
867
+ setPushStagedBusy(true); setMsg(null); setErr(null); setResult(null);
868
+ try {
869
+ const r = await jpost("/push-staged", { dir: dir || undefined, message: commitMsg.trim() });
870
+ setResult(r);
871
+ if (r.ok) {
872
+ const info = r.committed
873
+ ? "已提交「" + (r.message || "(自动生成)") + "」并推送" + (r.commitHash ? "(" + r.commitHash + ")" : "")
874
+ : "已推送本地已有的提交";
875
+ setMsg(info);
876
+ setCommitMsg("");
877
+ } else setErr(r.error || r.pushError || "推送失败");
878
+ // 推送成功后刷新状态(full 联网同步,显示「刷新中…」)。
879
+ void loadRepo(dir, true, true);
880
+ } catch (e) { setErr("推送失败:" + e); }
881
+ finally { setPushStagedBusy(false); }
882
+ }, [dir, commitMsg, loadRepo]);
883
+
908
884
  // 打开分支切换弹窗。
909
885
  const openBranches = useCallback(async () => {
910
886
  setDetail("branches"); setBranchBusy(true); setBranches([]);
@@ -1040,7 +1016,8 @@ window.__ModuleLoader__.load({
1040
1016
  setResult(null); setMsg(null); setErr(null);
1041
1017
  setDir(v);
1042
1018
  setDirDraft(v);
1043
- loadRepo(v);
1019
+ // full 同步:联网获取切换后工作区的最新状态,并显示「刷新中…」。
1020
+ loadRepo(v, true, true);
1044
1021
  },
1045
1022
  title: "选择 DSH 已登记的工作区文件夹",
1046
1023
  style: { flex: 1, font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
@@ -1055,7 +1032,7 @@ window.__ModuleLoader__.load({
1055
1032
  ),
1056
1033
  dir && customDirs && customDirs.includes(dir) ? h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 10, alignItems: "center" } },
1057
1034
  h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, "自定义目录:"),
1058
- h("span", { key: dir, style: { display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", border: "1px solid " + T.border, borderRadius: 12, fontSize: 12, background: T.layer1, color: T.label, cursor: "pointer" }, title: dir, onClick: () => { setResult(null); setMsg(null); setErr(null); setDir(dir); setDirDraft(dir); loadRepo(dir); } },
1035
+ h("span", { key: dir, style: { display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", border: "1px solid " + T.border, borderRadius: 12, fontSize: 12, background: T.layer1, color: T.label, cursor: "pointer" }, title: dir, onClick: () => { setResult(null); setMsg(null); setErr(null); setDir(dir); setDirDraft(dir); loadRepo(dir, true, true); } },
1059
1036
  String(dir).split(/[\\/]/).filter(Boolean).pop() || dir,
1060
1037
  h("span", {
1061
1038
  role: "button", "aria-label": "删除该目录记录",
@@ -1167,6 +1144,17 @@ window.__ModuleLoader__.load({
1167
1144
  : null
1168
1145
  return h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
1169
1146
  btns,
1147
+ // 「推送暂存」:放在「推送更改」与「刷新状态」之间,按需显示——
1148
+ // 有远程且「本地领先有提交」或「有已暂存的改动」时才出现(未装 better-sidebar 时)。
1149
+ (!hasBetterSidebar && repo && repo.hasRemote
1150
+ && (repo.ahead > 0 || ((repo.changedFiles || []).some((f) => f.staged)))) ? h(Btn, {
1151
+ key: "push-staged",
1152
+ label: pushStagedBusy ? "推送中…" : "推送暂存",
1153
+ onClick: doPushStaged,
1154
+ tone: "primary", noBg: true,
1155
+ disabled: busy || pushStagedBusy || !dir,
1156
+ title: "把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动",
1157
+ }) : null,
1170
1158
  // keepRepo=true 保留旧内容避免闪烁;full=true 触发 loadRepo 的「刷新中…」提示 + 联网同步。
1171
1159
  h(Btn, {
1172
1160
  label: refreshing ? "刷新中…" : "刷新状态",
@@ -1573,7 +1561,7 @@ window.__ModuleLoader__.load({
1573
1561
  }
1574
1562
 
1575
1563
  // ---------- the main panel ----------
1576
- // `variant` 决定布局:'drawer'(右侧栏抽屉,显示关闭按钮)与 'tab'
1564
+ // `variant` 决定布局:'drawer'(右侧栏集成面板,显示关闭按钮、填充容器)与 'tab'
1577
1565
  // (作为 dsh-better-sidebar 侧边栏 Tab 内容,填充容器、隐藏关闭按钮)。
1578
1566
  function ScmPanel({ onClose, variant }) {
1579
1567
  // 代码托管平台选择在②里操作、在③里跟随:lift 到面板级别共享。
@@ -1582,7 +1570,7 @@ window.__ModuleLoader__.load({
1582
1570
  return h("div", { style: panelStyle(variant), role: "dialog", "aria-modal": embedded ? undefined : "true", "aria-label": "源代码管理" },
1583
1571
  h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 } },
1584
1572
  h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, "源代码管理"),
1585
- embedded ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": "关闭", onClick: onClose }, "✕")
1573
+ variant === "tab" ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": "关闭", onClick: onClose }, "✕")
1586
1574
  ),
1587
1575
  h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
1588
1576
  "按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(" + (provider === "gitee" ? "Gitee" : "GitHub") + " 限制)并说明原因。"),
@@ -1595,8 +1583,8 @@ window.__ModuleLoader__.load({
1595
1583
  }
1596
1584
 
1597
1585
  function panelStyle(variant) {
1598
- // 'tab' / 'drawer':填充宿主容器(不设固定宽高、无边框阴影圆角),由外层
1599
- // dsh-better-sidebar 的 Tab 区或右侧抽屉)负责尺寸与表面。
1586
+ // 'tab' / 'drawer':填充宿主容器(不设固定宽高、无边框阴影圆角),
1587
+ // 由外层(dsh-better-sidebar 的 Tab 区或本插件的右侧面板)负责尺寸与表面。
1600
1588
  if (variant === "tab" || variant === "drawer") {
1601
1589
  return {
1602
1590
  position: "relative", display: "flex", flexDirection: "column", gap: 4,
@@ -1619,11 +1607,31 @@ window.__ModuleLoader__.load({
1619
1607
  return { appearance: "none", border: "none", background: "transparent", color: T.secondary, cursor: "pointer", fontSize: 18, lineHeight: 1, padding: "2px 6px", borderRadius: 6 };
1620
1608
  }
1621
1609
 
1622
- function maskStyle() {
1623
- return { position: "absolute", inset: 0, background: T.mask, backdropFilter: "var(--dsw-mask-blur, blur(4px))" };
1610
+ /** 注入的布局推挤样式(仅一次):右侧面板展开时把 #root 往左推,形成「集成侧边栏」,
1611
+ * 与 dsh-better-sidebar 右侧面板同一机制(margin + width calc)。 */
1612
+ const LAYOUT_CSS_ID = "source-code-mgmt-layout";
1613
+ let layoutCssInjected = false;
1614
+ function ensureLayoutCss() {
1615
+ if (layoutCssInjected) return;
1616
+ layoutCssInjected = true;
1617
+ const tag = document.createElement("style");
1618
+ tag.id = LAYOUT_CSS_ID;
1619
+ tag.setAttribute("data-source-code-mgmt-css", "");
1620
+ tag.textContent =
1621
+ // 推挤 #root:右侧面板占据布局而非浮在内容上方(VSCode 侧边栏手感),
1622
+ // 用 calc(100% - var) 避免桌面壳把 #root 设成 width:100% 时的加性溢出。
1623
+ "#root {" +
1624
+ " margin-right: var(--scm-push, 0px);" +
1625
+ " width: calc(100% - var(--scm-push, 0px));" +
1626
+ " transition: margin-right var(--ds-transition-duration-slow, .25s) var(--ds-ease-in-out, ease)," +
1627
+ " width var(--ds-transition-duration-slow, .25s) var(--ds-ease-in-out, ease);" +
1628
+ " }\n" +
1629
+ "body[data-source-code-mgmt-dragging] #root { transition: none; }\n" +
1630
+ "body[data-source-code-mgmt-dragging] { cursor: col-resize; user-select: none; }\n";
1631
+ document.head.appendChild(tag);
1624
1632
  }
1625
1633
 
1626
- /** 共享的「仓库 / 分支」小图标:既用于右上角浮动按钮,也用作侧边栏 Tab 图标。 */
1634
+ /** 共享的「仓库 / 分支」小图标:既用于左轨按钮,也用作侧边栏 Tab 图标。 */
1627
1635
  function repoIcon(size) {
1628
1636
  const s = size || 18;
1629
1637
  return h("svg", {
@@ -1637,47 +1645,101 @@ window.__ModuleLoader__.load({
1637
1645
  );
1638
1646
  }
1639
1647
 
1640
- /** 右上角浮动按钮样式(未安装 dsh-better-sidebar 时的入口)。 */
1641
- function floatBtnStyle() {
1648
+ // ---------- branch B:右上角 header 入口 + 右侧集成面板 ----------
1649
+ // 未安装 dsh-better-sidebar 时使用:把「代码管理」按钮注册进 DSH 的
1650
+ // conversation.session.header.utilities 槽位(Session log 所在的右对齐列表),
1651
+ // 因此它天然出现在 Session log 旁、样式一致的胶囊按钮、间距 8px 不挤在一起;
1652
+ // 点击后在右侧展开一个 dsh-better-sidebar 外观的固定面板(推挤 #root),复用 ScmPanel。
1653
+ const SCM_PANEL_W = 620;
1654
+
1655
+ /** Session log 同款胶囊按钮样式(描边、圆角 18、透明底),放在 header utilities 列表里。 */
1656
+ function headerBtnStyle() {
1642
1657
  return {
1643
- position: "fixed", top: 12, right: 16, zIndex: 2147483000,
1644
- display: "inline-flex", alignItems: "center", gap: 6,
1645
- height: 32, padding: "0 12px", border: "none", borderRadius: 16,
1646
- background: T.brand, color: "#fff", cursor: "pointer",
1647
- fontSize: 13, lineHeight: 1,
1648
- boxShadow: "var(--dsw-overlay-shadow, 0 4px 12px rgba(0,0,0,.3))",
1658
+ display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 4,
1659
+ height: 32, padding: "6px 12px",
1660
+ border: "1px solid " + T.border, borderRadius: 18,
1661
+ color: T.label, background: "transparent",
1662
+ font: "inherit", fontSize: 13, fontWeight: 400, lineHeight: 1,
1663
+ cursor: "pointer", whiteSpace: "nowrap",
1649
1664
  };
1650
1665
  }
1651
1666
 
1652
- /** 右侧栏抽屉外层样式:右对齐、全高、固定宽度。 */
1653
- function drawerStyle() {
1654
- return {
1655
- position: "relative", zIndex: 1, height: "100%", width: 640, maxWidth: "100vw",
1656
- marginLeft: "auto", display: "flex", flexDirection: "column",
1657
- boxSizing: "border-box", padding: 0,
1658
- background: "var(--dsw-alias-bg-layer-3, #fff)", color: T.label,
1659
- borderLeft: "1px solid " + T.border,
1660
- boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))",
1667
+ /** 右侧面板的最小 / 最大宽度(拖拽改宽时钳制,防止拖得过窄或超出视口)。 */
1668
+ const SCM_PANEL_MIN = 360;
1669
+ const SCM_PANEL_MAX_FRAC = 0.92; // 拖拽上限:视口宽度的 92%
1670
+
1671
+ /** 右侧集成面板拖拽改宽:拖动面板左边缘调整宽度(像 dsh-better-sidebar)。要点:
1672
+ * 拖拽期间逐帧直接写 DOM(面板 style.width + --scm-push CSS 变量),避免每帧触发
1673
+ * React 重渲染造成卡顿;在 pointerup 时把最终宽度提交进 state,重开面板仍保持。 */
1674
+ function makeResizeHandler(getPanelEl, getW, onCommit) {
1675
+ return (e) => {
1676
+ e.preventDefault();
1677
+ const startX = e.clientX;
1678
+ const startW = getW();
1679
+ let lastW = startW;
1680
+ const body = document.body;
1681
+ body.setAttribute("data-source-code-mgmt-dragging", "");
1682
+ const onMove = (ev) => {
1683
+ // 面板贴右(right:0),往左拖 = 变宽;增量 = 起点X - 当前X。
1684
+ let w = startW + (startX - ev.clientX);
1685
+ const max = Math.round(window.innerWidth * SCM_PANEL_MAX_FRAC);
1686
+ w = Math.round(Math.max(SCM_PANEL_MIN, Math.min(max, w)));
1687
+ lastW = w;
1688
+ const el = getPanelEl();
1689
+ if (el) el.style.width = w + "px";
1690
+ document.documentElement.style.setProperty("--scm-push", w + "px");
1691
+ };
1692
+ const onUp = () => {
1693
+ body.removeAttribute("data-source-code-mgmt-dragging");
1694
+ window.removeEventListener("pointermove", onMove);
1695
+ window.removeEventListener("pointerup", onUp);
1696
+ if (lastW !== startW) onCommit(lastW);
1697
+ };
1698
+ window.addEventListener("pointermove", onMove);
1699
+ window.addEventListener("pointerup", onUp);
1661
1700
  };
1662
1701
  }
1663
1702
 
1664
- // ---------- branch B:右上角浮动按钮 + 右侧抽屉入口 ----------
1665
- // 未安装 dsh-better-sidebar 时使用:一个紧贴右上角的浮动按钮,点击后展开
1666
- // 一个右侧栏抽屉(形态类似 dsh-better-sidebar 的右栏),内容放现有 ScmPanel。
1667
- function ScmFloatingEntry() {
1703
+ /** 右上角「代码管理」按钮 + 点击后打开的右侧集成面板(dsh-better-sidebar 外观,
1704
+ * 可拖拽左边缘调整宽度)。 */
1705
+ function HeaderScmAction() {
1668
1706
  const [open, setOpen] = useState(false);
1707
+ const [panelW, setPanelW] = useState(SCM_PANEL_W);
1708
+ const panelRef = useRef(null);
1709
+ useEffect(() => { ensureLayoutCss(); }, []);
1710
+ // 右侧面板打开时推挤 #root(margin-right),关闭时归零。宽度跟随 panelW。
1711
+ useEffect(() => {
1712
+ document.documentElement.style.setProperty("--scm-push", open ? panelW + "px" : "0px");
1713
+ return () => { document.documentElement.style.removeProperty("--scm-push"); };
1714
+ }, [open, panelW]);
1715
+ // 拖拽改宽处理器(面板左边缘的拖拽条)。
1716
+ const onResize = makeResizeHandler(
1717
+ () => panelRef.current,
1718
+ () => panelW,
1719
+ (w) => setPanelW(w),
1720
+ );
1669
1721
  return h(React.Fragment, null,
1670
- h("button", {
1671
- type: "button",
1672
- style: floatBtnStyle(),
1673
- title: "源代码管理",
1674
- "aria-label": "源代码管理",
1675
- onClick: () => setOpen(true),
1676
- }, repoIcon(16), h("span", { style: { fontSize: 13 } }, "代码管理")),
1722
+ h("button", { type: "button", style: headerBtnStyle(), title: "源代码管理", "aria-label": "源代码管理", onClick: () => setOpen(true) },
1723
+ repoIcon(14), h("span", { style: { fontSize: 13 } }, "代码管理")),
1677
1724
  open ? ReactDOM.createPortal(
1678
- h("div", { style: { position: "fixed", inset: 0, zIndex: 1000 }, role: "presentation" },
1679
- h("div", { style: maskStyle(), "aria-hidden": "true", onClick: () => setOpen(false) }),
1680
- h("div", { style: drawerStyle() },
1725
+ h("div", { style: { position: "fixed", inset: 0, zIndex: 1000, pointerEvents: "none" }, role: "presentation" },
1726
+ h("div", { ref: panelRef, style: {
1727
+ position: "absolute", top: 0, right: 0, bottom: 0, width: panelW + "px",
1728
+ boxSizing: "border-box", overflow: "visible", pointerEvents: "auto",
1729
+ background: "var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08))",
1730
+ borderLeft: "1px solid " + T.border,
1731
+ boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))",
1732
+ } },
1733
+ // 左边缘拖拽条(改宽)
1734
+ h("div", {
1735
+ onPointerDown: onResize,
1736
+ title: "拖动调整面板宽度",
1737
+ "aria-label": "拖动调整面板宽度",
1738
+ style: {
1739
+ position: "absolute", top: 0, bottom: 0, left: -3, width: 7,
1740
+ cursor: "col-resize", pointerEvents: "auto", zIndex: 1,
1741
+ },
1742
+ }),
1681
1743
  h(ScmPanel, { variant: "drawer", onClose: () => setOpen(false) })
1682
1744
  )
1683
1745
  ),
@@ -1737,28 +1799,40 @@ window.__ModuleLoader__.load({
1737
1799
  single: true,
1738
1800
  component: () => h(ScmPanel, { variant: "tab" }),
1739
1801
  }));
1740
- // 若此前已挂载了分支 B 的浮动按钮,立即拆除它。
1802
+ // 若此前已挂载了分支 B 的 header 入口(降级路径),立即拆除它。
1741
1803
  if (entryUnmount) { entryUnmount(); entryUnmount = null; }
1742
1804
  return true;
1743
1805
  };
1744
1806
 
1745
1807
  if (tryRegisterTab()) return;
1746
1808
 
1747
- // 分支 B:此刻未检测到 dsh-better-sidebar —— 右上角浮动按钮 + 右侧栏抽屉。
1748
- if (!ReactDOM) return;
1749
- ctx.effect(() => {
1750
- const hostEl = document.createElement("div");
1751
- hostEl.setAttribute("data-source-code-mgmt-entry", "");
1752
- document.body.appendChild(hostEl);
1753
- const root = mountClientRoot(hostEl, h(ScmFloatingEntry));
1754
- entryUnmount = () => {
1755
- try { if (root && typeof root.unmount === "function") root.unmount(); } catch {}
1756
- hostEl.remove();
1757
- };
1758
- return () => { if (entryUnmount) { entryUnmount(); entryUnmount = null; } };
1759
- });
1809
+ // 分支 B:此刻未检测到 dsh-better-sidebar —— 把「代码管理」按钮注册进 DSH
1810
+ // conversation.session.header.utilities 槽位(Session log 所在的右对齐列表),
1811
+ // 因此它天然出现在 Session log 旁、间距 8px 不挤在一起;点击后 HeaderScmAction
1812
+ // 打开右侧 dsh-better-sidebar 外观的集成面板(推挤 #root)。
1813
+ const slots = typeof ctx.get === "function" ? ctx.get("slots") : undefined;
1814
+ if (slots && typeof slots.inject === "function") {
1815
+ ctx.effect(() => slots.inject('conversation.session.header.utilities', () => slots.register({
1816
+ name: 'conversation.session.header.utilities',
1817
+ id: 'source-code-mgmt',
1818
+ order: 200,
1819
+ }, HeaderScmAction)), 'source-code-mgmt: header utility');
1820
+ } else if (ReactDOM) {
1821
+ // slots 服务不可用(极少数环境)——降级:右上角浮动按钮 + 右侧面板。
1822
+ ctx.effect(() => {
1823
+ const hostEl = document.createElement("div");
1824
+ hostEl.setAttribute("data-source-code-mgmt-entry", "");
1825
+ document.body.appendChild(hostEl);
1826
+ const root = mountClientRoot(hostEl, h(HeaderScmAction));
1827
+ entryUnmount = () => {
1828
+ try { if (root && typeof root.unmount === "function") root.unmount(); } catch {}
1829
+ hostEl.remove();
1830
+ };
1831
+ return () => { if (entryUnmount) { entryUnmount(); entryUnmount = null; } };
1832
+ });
1833
+ }
1760
1834
 
1761
- // 兜底重试一次:better-sidebar 若在本插件之后激活,延迟补注册 Tab 并拆除浮动按钮。
1835
+ // 兜底重试一次:better-sidebar 若在本插件之后激活,延迟补注册 Tab 并拆除 header 入口。
1762
1836
  // 单次 setTimeout,无轮询、零 I/O,几乎不耗资源;插件卸载时清除。
1763
1837
  const retryTimer = window.setTimeout(() => { tryRegisterTab(); }, 1500);
1764
1838
  ctx.effect(() => () => window.clearTimeout(retryTimer));
package/lib/index.js CHANGED
@@ -1402,6 +1402,62 @@ function commitFlow(dir, message, paths) {
1402
1402
  }
1403
1403
  }
1404
1404
 
1405
+ /**
1406
+ * Push STAGED changes only: commit whatever is currently staged (using the
1407
+ * given message, or an autogenerated one when empty), then push to origin.
1408
+ * Unlike {@link pushFlow} it does NOT go on to stage untracked/unstaged files
1409
+ * itself and does NOT overwrite the user's own commit message. When nothing is
1410
+ * newly staged yet (e.g. the local branch already has commits ahead), it simply
1411
+ * pushes those existing commits.
1412
+ * @param {string} dir - local folder.
1413
+ * @param {string} [message] - commit message used only when something is staged
1414
+ * and the caller supplies one; empty falls back to an autogenerated message.
1415
+ */
1416
+ function pushStagedFlow(dir, message) {
1417
+ const guard = requireGitRepo(dir)
1418
+ if (guard) return { ok: false, error: guard.error }
1419
+
1420
+ // Remote must exist before we can push.
1421
+ const hasRemote = run(GIT, ['-C', dir, 'remote', 'get-url', 'origin']).ok
1422
+ if (!hasRemote) {
1423
+ return { ok: false, needsRemote: true, error: '尚未配置远程仓库 origin,请使用「新建仓库」创建远程仓库。' }
1424
+ }
1425
+
1426
+ const msg = String(message || '').trim()
1427
+ let committed = false
1428
+ let commitHash
1429
+ // Commit staged content only if there is anything staged right now.
1430
+ const hasStaged = run(GIT, ['-C', dir, 'diff', '--cached', '--quiet']).code !== 0
1431
+ if (hasStaged) {
1432
+ const ident = run(GIT, ['-C', dir, 'config', 'user.email'])
1433
+ if (!ident.ok) {
1434
+ run(GIT, ['-C', dir, 'config', 'user.name', 'DSH User'])
1435
+ run(GIT, ['-C', dir, 'config', 'user.email', 'dsh@localhost'])
1436
+ }
1437
+ const useMsg = msg || ('chore: update ' + baseName(dir))
1438
+ const commit = run(GIT, ['-C', dir, 'commit', '-m', useMsg])
1439
+ if (!commit.ok) return { ok: false, error: (commit.stderr || commit.stdout).trim() || 'git commit 失败' }
1440
+ committed = true
1441
+ const rev = run(GIT, ['-C', dir, 'rev-parse', '--short', 'HEAD'])
1442
+ commitHash = rev.ok ? rev.stdout.trim() : undefined
1443
+ }
1444
+
1445
+ const curBranch = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
1446
+ const branchName = curBranch.ok ? curBranch.stdout.trim() : 'main'
1447
+ const push = run(GIT, ['-C', dir, 'push', '-u', 'origin', branchName], { timeout: 120_000 })
1448
+ const pushed = push.ok
1449
+ return {
1450
+ ok: pushed,
1451
+ needsRemote: false,
1452
+ committed,
1453
+ commitHash,
1454
+ message: msg || (committed ? ('chore: update ' + baseName(dir)) : undefined),
1455
+ branch: branchName,
1456
+ pushed,
1457
+ pushError: pushed ? undefined : (push.stderr || push.stdout).trim(),
1458
+ }
1459
+ }
1460
+
1405
1461
  /**
1406
1462
  * List branches (current first) using `git for-each-ref`.
1407
1463
  * @param {string} dir - local folder.
@@ -2099,6 +2155,13 @@ export function apply(ctx) {
2099
2155
  return pushFlow(body.dir || fallbackDir)
2100
2156
  })
2101
2157
  },
2158
+ '/push-staged': async (req, res) => {
2159
+ if (req.method !== 'POST') return methodNotAllowed(res, req.method)
2160
+ await handle(req, res, async (req) => {
2161
+ const body = JSON.parse((await readBody(req)) || '{}')
2162
+ return pushStagedFlow(body.dir || fallbackDir, body.message)
2163
+ })
2164
+ },
2102
2165
  '/pull': async (req, res) => {
2103
2166
  if (req.method !== 'POST') return methodNotAllowed(res, req.method)
2104
2167
  await handle(req, res, async (req) => {
package/package.json CHANGED
@@ -1,38 +1,38 @@
1
- {
2
- "name": "source-code-mgmt",
3
- "version": "1.5.0",
4
- "description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a top-right floating button + right drawer — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, and auto-ignore files over 100MB with per-file skipped reasons. Cross-platform (Windows / Linux / macOS).",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": {
9
- "default": "./lib/index.js"
10
- },
11
- "./client": {
12
- "default": "./lib/client.js"
13
- },
14
- "./package.json": "./package.json"
15
- },
16
- "dsh": {
17
- "bundle": "./lib/index.js",
18
- "client": {
19
- "inject": [
20
- "@deepseek-ai/dsh-client-runtime"
21
- ],
22
- "platform": "web"
23
- }
24
- },
25
- "peerDependencies": {
26
- "react": "^18.2.0",
27
- "dsh-better-sidebar": ">=0.4.0"
28
- },
29
- "peerDependenciesMeta": {
30
- "dsh-better-sidebar": {
31
- "optional": true
32
- }
33
- },
34
- "files": [
35
- "lib/**/*.js"
36
- ],
37
- "license": "MIT"
38
- }
1
+ {
2
+ "name": "source-code-mgmt",
3
+ "version": "1.8.0",
4
+ "description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a button in the DSH session header's right-aligned utilities (beside \"Session log\") opening a right-side integrated panel that pushes the main content — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, and auto-ignore files over 100MB with per-file skipped reasons. Cross-platform (Windows / Linux / macOS).",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./lib/index.js"
10
+ },
11
+ "./client": {
12
+ "default": "./lib/client.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "bundle": "./lib/index.js",
18
+ "client": {
19
+ "inject": [
20
+ "@deepseek-ai/dsh-client-runtime"
21
+ ],
22
+ "platform": "web"
23
+ }
24
+ },
25
+ "peerDependencies": {
26
+ "react": "^18.2.0",
27
+ "dsh-better-sidebar": ">=0.4.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "dsh-better-sidebar": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "files": [
35
+ "lib/**/*.js"
36
+ ],
37
+ "license": "MIT"
38
+ }