@qzsy/dsh-git-panel 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +10 -2
  2. package/lib/client.js +530 -270
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,13 +8,16 @@
8
8
 
9
9
  在右侧栏的引导页(`+` → 卡片)里多一张 **Git** 卡片,点开就是 Git 分区:
10
10
 
11
- - **顶部**:仓库名 / 当前分支胶囊(点开可切换分支)、游离 HEAD、领先/落后数、抓取、同步、刷新。
11
+ - **顶部就是提交框**:不再有独立工具条;仓库动作全部并进了分支图顶栏(见下)。
12
12
  - **提交框**:提交信息输入框,`Ctrl/⌘+Enter` 直接提交;主按钮右侧的小箭头里有「提交(已暂存)」「提交全部改动」「修订上一次提交」「跳过校验钩子提交」「提交后推送」;右侧 `⋯` 里是「撤销上一次提交」「暂存改动」「恢复暂存的改动」。
13
13
  - **CHANGES 分区**:分组列出 冲突 / 已暂存 / 未暂存 / 未跟踪,每行带**文件类型图标**(代码 / 配置 / 样式 / 文档 / 图片 / 音视频 / 字体 / 归档 / 脚本 / 锁文件 / 普通文件)与**改动类型图标**(修改 / 新增 / 未跟踪 / 删除 / 重命名 / 复制 / 类型变更 / 冲突,颜色与虚线圈区分暂存与否)、`+/-` 行数;悬停出现暂存、取消暂存、丢弃、看改动、打开文件五个动作;分区标题右侧是全部暂存 / 全部取消暂存 / 全部丢弃 / 预览全部改动。
14
- - **GRAPH 分区**:泳道图(合并节点画空心圈)+ 分支/标签/远端徽标 + 摘要 + 作者 + 相对时间;点一行展开提交说明面板(作者、时间、hash、正文、文件清单与 `+/-`),并给出「查看这次提交的改动」「复制 hash」「切换到这个分支」。标题栏可切「当前分支 / 全部引用」。
14
+ - **GRAPH 分区**:泳道图(合并节点画空心圈)+ 分支/标签/远端徽标 + 摘要 + 作者 + 相对时间;点一行展开提交说明面板(作者、时间、hash、正文、文件清单与 `+/-`),并给出「查看这次提交的改动」「复制 hash」「切换到这个分支」。
15
+ - **分支图顶栏**:左边是折叠热区(chevron + 「分支图」+ 计数),右边是仓库动作——当前分支胶囊(内含 `↑ahead / ↓behind`,点开向上弹出本地分支菜单)、「当前分支 / 全部引用」切换、抓取、同步、刷新。折叠热区只覆盖左侧标签簇,不会误触右侧按钮;面板窄于 300px 时自动隐藏「抓取」(同步已经包含 fetch)。
15
16
  - **改动预览标签页**:点文件行 → 新标签显示这个文件的统一 diff(带双行号),标题就是这个文件的路径;未跟踪的新文件没有 index 版本,由 Host 直接合成「整篇新增」的补丁,所以看到的是全绿;「预览全部改动」把已暂存、未暂存、新文件拼成一张总 diff。
16
17
  - **打开文件标签页**:文件行的「打开文件」走侧栏自己的 `openResource`(`dsh-resource://file/session/...`),落进任何文件链接都会用的那个预览器。
17
18
 
19
+ 样式按「Apple 语义色 + 源码控制图布局」重做:表面/文字/分隔线全部取宿主的 `--dsw-alias-*` 语义 token,亮色与暗色主题自动跟随,不再假定深色;行高、计数、状态芯片、弹层和 diff 行号都按同一套 4/8pt 间距重排。
20
+
18
21
  所有丢弃、撤销提交都要点一次确认才执行——不会一点就掉改动。「丢弃」的确认是面板底部的一次性浮层(不随列表滚走,Esc 或点空白处取消);确认后真的执行:已跟踪文件回到 HEAD,未跟踪与「已暂存的新文件」连内容一起删掉,列表随即刷新。
19
22
 
20
23
  ## 结构
@@ -25,6 +28,11 @@ lib/client.js 浏览器半:两个标签页类型 + 面板与预览页
25
28
  test/ host 侧真仓库行为测试 + client 侧注册契约测试
26
29
  ```
27
30
 
31
+ **改包名时必须同时改两处**:`cordis.patch.yml` 的 `insert[].name`,以及 `lib/client.js` 里
32
+ `window.__ModuleLoader__.load({ id })` 的 `id`。web loader 按包名去抓脚本,脚本注册的名字和包名
33
+ 不一致会被判成「loaded without registering」,那是整页插件加载失败(白屏),不是只掉一个分区。
34
+ `test/client.test.mjs` 现在直接拿 `package.json` 的 `name` 断言这个 id,改名漏了会先在这里红。
35
+
28
36
  Host 半只做 git:读(`status` / `graph` / `diff` / `commitDetail` / `branches`)与写(`stage` / `unstage` / `discard` / `commit` / `undoCommit` / `network` / `checkout` / `stash`)。浏览器半只渲染和调它。
29
37
 
30
38
  安全边界都在 Host 半:
package/lib/client.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * same previewer any other file link uses.
17
17
  */
18
18
  window.__ModuleLoader__.load({
19
- id: 'dsh-git-panel',
19
+ id: '@qzsy/dsh-git-panel',
20
20
  factory(require) {
21
21
  const React = require('react')
22
22
  const h = React.createElement
@@ -123,6 +123,7 @@ window.__ModuleLoader__.load({
123
123
  'branch.behind': '落后 {count}',
124
124
  'branch.switch': '切换分支',
125
125
  'branch.current': '当前分支',
126
+ 'branch.local': '本地分支',
126
127
  'status.refresh': '刷新',
127
128
  'status.fetch': '抓取',
128
129
  'status.pull': '拉取',
@@ -159,7 +160,7 @@ window.__ModuleLoader__.load({
159
160
  'confirm.discard': '丢弃',
160
161
  'confirm.cancel': '取消',
161
162
  'discard.done': '已丢弃 {count} 个文件的改动',
162
- 'commit.placeholder': '提交信息({mod}+↵ 提交到 “{branch}”)',
163
+ 'commit.placeholder': '提交信息({mod}+↵ 提交)',
163
164
  'commit.draft': '按当前改动生成提交信息草稿',
164
165
  'commit.draft.one': '更新 {name}',
165
166
  'commit.draft.many': '更新 {name} 等 {count} 个文件',
@@ -235,6 +236,7 @@ window.__ModuleLoader__.load({
235
236
  'branch.behind': '{count} behind',
236
237
  'branch.switch': 'Switch branch',
237
238
  'branch.current': 'Current branch',
239
+ 'branch.local': 'Local branches',
238
240
  'status.refresh': 'Refresh',
239
241
  'status.fetch': 'Fetch',
240
242
  'status.pull': 'Pull',
@@ -271,7 +273,7 @@ window.__ModuleLoader__.load({
271
273
  'confirm.discard': 'Discard',
272
274
  'confirm.cancel': 'Cancel',
273
275
  'discard.done': 'Discarded changes in {count} files',
274
- 'commit.placeholder': 'Message ({mod}+↵ to commit on "{branch}")',
276
+ 'commit.placeholder': 'Message ({mod}+↵ to commit)',
275
277
  'commit.draft': 'Draft a commit message from the changes',
276
278
  'commit.draft.one': 'Update {name}',
277
279
  'commit.draft.many': 'Update {name} and {count} more files',
@@ -325,181 +327,302 @@ window.__ModuleLoader__.load({
325
327
  /* ------------------------------------------------------------------ */
326
328
 
327
329
  const CSS = `
328
- /* Apple-flavoured design tokens, scoped to the panel. */
330
+ /* ---------------------------------------------------------------------------
331
+ A Git pane in the Apple surface language, laid out like the Source Control
332
+ Graph: one header row per pane, a compact list, lane artwork on the left.
333
+
334
+ Colour comes from the host app's semantic tokens, so the panel follows the
335
+ app's light and dark themes; the value after each comma is the dark-theme
336
+ fallback used when the panel renders outside the app shell.
337
+ --------------------------------------------------------------------------- */
329
338
  .gp-root{
330
339
  --gp-font:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
331
340
  --gp-mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,monospace;
332
- --gp-label:rgba(255,255,255,.92);
333
- --gp-label-2:rgba(235,235,245,.62);
334
- --gp-label-3:rgba(235,235,245,.38);
335
- --gp-label-4:rgba(235,235,245,.20);
336
- --gp-fill-2:rgba(120,120,128,.28);
337
- --gp-fill-3:rgba(120,120,128,.20);
338
- --gp-fill-4:rgba(120,120,128,.12);
339
- --gp-separator:rgba(84,84,88,.62);
340
- --gp-accent:#80a1c0;
341
- --gp-accent-hover:#8db0cf;
342
- --gp-accent-dim:#3a4552;
343
- --gp-accent-ink:#101720;
344
- --gp-red:#ff453a;
345
- --gp-green:#30d158;
346
- --gp-orange:#ff9f0a;
347
- --gp-yellow:#ffd60a;
348
- --gp-blue:#0a84ff;
349
- --gp-purple:#bf5af2;
350
- --gp-radius:8px;
341
+
342
+ --gp-bg:var(--dsw-alias-bg-base,#151517);
343
+ --gp-surface:var(--dsw-alias-bg-layer-1,#1b1b1d);
344
+ --gp-surface-2:var(--dsw-alias-bg-layer-2,#202023);
345
+ --gp-surface-3:var(--dsw-alias-bg-layer-3,#26262a);
346
+ --gp-label:var(--dsw-alias-label-primary,#f5f5f7);
347
+ --gp-label-2:var(--dsw-alias-label-secondary,#c7c7cc);
348
+ --gp-label-3:var(--dsw-alias-label-tertiary,#8e8e93);
349
+ --gp-label-4:var(--dsw-alias-label-caption,#5f5f66);
350
+ --gp-separator:var(--dsw-alias-border-l2,rgba(255,255,255,.12));
351
+ --gp-hairline:var(--dsw-alias-border-l1,rgba(255,255,255,.07));
352
+ --gp-hover:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.07));
353
+ --gp-press:var(--dsw-alias-interactive-bg-active,rgba(255,255,255,.12));
354
+
355
+ --gp-green:var(--dsw-alias-state-success-primary,#30d158);
356
+ --gp-red:var(--dsw-alias-state-error-primary,#ff453a);
357
+ --gp-orange:#b26a00;
358
+ --gp-purple:#8b3fd1;
359
+ --gp-blue:#1f6feb;
360
+ --gp-code-add:var(--dsw-alias-code-diff-added,rgba(48,209,88,.14));
361
+ --gp-code-del:var(--dsw-alias-code-diff-deleted,rgba(255,69,58,.15));
362
+
363
+ --gp-file-code:#a87c1f;
364
+ --gp-file-data:#7a5cc4;
365
+ --gp-file-style:#1c7fa8;
366
+ --gp-file-doc:#3f74a8;
367
+ --gp-file-image:#9a55b8;
368
+ --gp-file-media:#b8457d;
369
+ --gp-file-font:#8a7420;
370
+ --gp-file-archive:#6b7079;
371
+ --gp-file-lock:#6b6c73;
372
+ --gp-file-shell:#2f7d4a;
373
+
374
+ --gp-accent:#2f6fb3;
375
+ --gp-accent-hover:#255d97;
376
+ --gp-accent-ink:#ffffff;
377
+ --gp-accent-soft:rgba(47,111,179,.12);
378
+ --gp-ring:0 0 0 3px rgba(47,111,179,.3);
379
+
380
+ --gp-radius-s:6px;
381
+ --gp-radius-m:9px;
382
+ --gp-radius-l:12px;
383
+ --gp-ease:cubic-bezier(.32,.72,0,1);
384
+
351
385
  display:flex;flex-direction:column;height:100%;min-height:0;position:relative;
386
+ container-type:inline-size;
352
387
  font-family:var(--gp-font);font-size:12.5px;line-height:1.45;letter-spacing:-.006em;
353
388
  color:var(--gp-label);background:transparent;
354
389
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
355
390
  }
356
- .gp-scroll{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}
357
- .gp-review{flex:none;position:relative;display:flex;flex-direction:column;gap:6px;padding:8px 10px}
358
- .gp-review-btn{width:100%;height:28px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:none;border-radius:7px;background:var(--gp-fill-3);color:var(--gp-label);font:inherit;font-size:12.5px;font-weight:560;cursor:pointer;transition:background .12s ease-out}
359
- .gp-review-btn:hover:not(:disabled){background:var(--gp-fill-2)}
391
+ body[data-ds-dark-theme] .gp-root{
392
+ --gp-orange:#ffb340;
393
+ --gp-purple:#c08cff;
394
+ --gp-blue:#66a9ff;
395
+ --gp-file-code:#e0c07a;
396
+ --gp-file-data:#b9a3e3;
397
+ --gp-file-style:#6fc7e8;
398
+ --gp-file-doc:#7fa6cf;
399
+ --gp-file-image:#c08ed6;
400
+ --gp-file-media:#e08ab0;
401
+ --gp-file-font:#d8c98a;
402
+ --gp-file-archive:#9aa3ad;
403
+ --gp-file-lock:#a9a9b3;
404
+ --gp-file-shell:#8fd39a;
405
+ --gp-accent:#9dbcd8;
406
+ --gp-accent-hover:#b2cbe2;
407
+ --gp-accent-ink:#0d1319;
408
+ --gp-accent-soft:rgba(157,188,216,.16);
409
+ --gp-ring:0 0 0 3px rgba(157,188,216,.3);
410
+ }
411
+ .gp-root ::-webkit-scrollbar{width:9px;height:9px}
412
+ .gp-root ::-webkit-scrollbar-track{background:transparent}
413
+ .gp-root ::-webkit-scrollbar-thumb{background:var(--dsw-alias-scrollbar-bg-l2,#3b3b40);border-radius:999px;border:2px solid transparent;background-clip:content-box}
414
+ .gp-root ::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-scrollbar-hover-l2,#4a4a50);background-clip:content-box}
415
+
416
+ .gp-changes,.gp-graph-list,.gp-diff-body{overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,#3b3b40) transparent}
417
+
418
+ /* --- review --------------------------------------------------------------- */
419
+ .gp-review{flex:none;display:flex;flex-direction:column;gap:6px;padding:8px 12px 6px}
420
+ .gp-review-head{display:flex;align-items:center;gap:6px;align-self:flex-start;font-size:10.5px;font-weight:650;letter-spacing:.05em;text-transform:uppercase;color:var(--gp-label-3);cursor:pointer;user-select:none;padding:2px 6px;margin-left:-6px;border-radius:var(--gp-radius-s);transition:background .12s var(--gp-ease),color .12s var(--gp-ease)}
421
+ .gp-review-head:hover{background:var(--gp-hover);color:var(--gp-label-2)}
422
+ .gp-review-head:focus-visible{outline:none;box-shadow:var(--gp-ring)}
423
+ .gp-review-btn{width:100%;height:28px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:.5px solid var(--gp-separator);border-radius:var(--gp-radius-s);background:transparent;color:var(--gp-label-2);font:inherit;font-size:12px;font-weight:560;cursor:pointer;transition:background .12s var(--gp-ease),color .12s var(--gp-ease),border-color .12s var(--gp-ease)}
424
+ .gp-review-btn:hover:not(:disabled){background:var(--gp-hover);border-color:var(--gp-label-4);color:var(--gp-label)}
425
+ .gp-review-btn:active:not(:disabled){transform:scale(.995)}
360
426
  .gp-review-btn:disabled{opacity:.42;cursor:default}
361
- .gp-review-head{font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;color:var(--gp-label-3);cursor:pointer;user-select:none}
362
- .gp-review-head:hover{color:var(--gp-label-2)}
363
- .gp-changes{flex:1;min-height:80px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}
364
- .gp-resizer{flex:none;position:relative;height:8px;cursor:row-resize;background:transparent;user-select:none;touch-action:none}
365
- .gp-resizer:hover,.gp-resizer:active{background:var(--gp-fill-3)}
366
- .gp-graph-panel{flex:none;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}
367
- .gp-toolbar{display:flex;align-items:center;gap:6px;flex:none;padding:8px 10px;border-bottom:.5px solid var(--gp-separator)}
368
- .gp-toolbar .gp-spacer{flex:1}
369
- .gp-branch{display:inline-flex;align-items:center;gap:5px;min-width:0;max-width:100%;height:22px;padding:0 8px;border-radius:6px;background:var(--gp-fill-3);color:var(--gp-label);font-family:var(--gp-mono);font-size:11px;letter-spacing:0;border:none;cursor:pointer;transition:background .12s ease-out}
370
- .gp-branch:hover{background:var(--gp-fill-2)}
371
- .gp-branch span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
372
- .gp-tag{font-size:10.5px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--gp-fill-3);color:var(--gp-label-2)}
373
- .gp-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;height:24px;min-width:24px;padding:0 6px;border-radius:6px;border:none;background:transparent;color:var(--gp-label-2);font:inherit;font-size:11.5px;cursor:pointer;transition:background .12s ease-out,color .12s ease-out}
374
- .gp-btn:hover:not(:disabled){background:var(--gp-fill-3);color:var(--gp-label)}
375
- .gp-btn:active:not(:disabled){background:var(--gp-fill-2)}
376
- .gp-btn:disabled{opacity:.38;cursor:default}
377
- .gp-btn svg{width:13px;height:13px;flex:none}
378
- .gp-btn-danger:hover:not(:disabled){background:rgba(255,69,58,.18);color:var(--gp-red)}
379
- .gp-primary{height:28px;padding:0 12px;border-radius:7px;border:none;background:var(--gp-accent);color:var(--gp-accent-ink);font:inherit;font-size:12.5px;font-weight:590;letter-spacing:-.01em;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background .12s ease-out,transform .06s ease-out}
380
- .gp-primary:hover:not(:disabled){background:var(--gp-accent-hover)}
381
- .gp-primary:active:not(:disabled){transform:scale(.985)}
382
- .gp-primary:disabled{background:var(--gp-accent-dim);color:var(--gp-label-3);cursor:default}
383
- .gp-compose{flex:none;display:flex;flex-direction:column;gap:8px;padding:10px}
384
- .gp-input{box-sizing:border-box;width:100%;min-height:36px;max-height:132px;resize:none;field-sizing:content;padding:8px 10px;border-radius:var(--gp-radius);border:.5px solid rgba(255,255,255,.07);background:#2e2e2e;color:rgba(255,255,255,.94);font:inherit;font-size:12.5px;line-height:1.45;outline:none;transition:border-color .12s ease-out,box-shadow .12s ease-out}
385
- .gp-input:focus{border-color:var(--gp-accent);box-shadow:0 0 0 3.5px rgba(128,161,192,.28)}
386
- .gp-input::placeholder{color:var(--gp-label-3)}
387
- .gp-compose .gp-input-wrap{position:relative;display:flex}
388
- .gp-input-wrap .gp-input{padding-right:34px}
389
- .gp-input-ai{position:absolute;top:5px;right:5px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;border-radius:6px;background:transparent;color:var(--gp-label-3);cursor:pointer;transition:background .12s ease-out,color .12s ease-out}
390
- .gp-input-ai:hover:not(:disabled){background:var(--gp-fill-3);color:var(--gp-label)}
391
- .gp-input-ai:disabled{opacity:.35;cursor:default}
392
- .gp-input-ai svg{width:14px;height:14px;flex:none}
393
- .gp-actions{display:flex;align-items:center;gap:6px}
394
- .gp-actions > .gp-primary{flex:1}
395
- .gp-actions .gp-primary{width:100%;height:32px;justify-content:center;gap:6px;font-size:13px;border-radius:8px}
396
- .gp-actions .gp-primary svg{width:14px;height:14px;flex:none}
397
- .gp-actions .gp-primary-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
398
- .gp-menu-wrap{position:relative}
399
- .gp-menu{position:absolute;z-index:40;top:calc(100% + 6px);right:0;min-width:200px;padding:5px;border-radius:10px;border:.5px solid rgba(255,255,255,.1);background:rgba(40,40,44,.86);backdrop-filter:saturate(180%) blur(24px);-webkit-backdrop-filter:saturate(180%) blur(24px);box-shadow:0 12px 32px rgba(0,0,0,.48),0 0 0 .5px rgba(0,0,0,.5);display:flex;flex-direction:column;gap:1px}
400
- .gp-menu button{display:flex;width:100%;align-items:center;gap:7px;height:28px;padding:0 8px;border:none;border-radius:6px;background:transparent;color:var(--gp-label);font:inherit;font-size:12.5px;text-align:left;cursor:pointer;transition:background .1s ease-out}
401
- .gp-menu button:hover:not(:disabled){background:var(--gp-accent)}
402
- .gp-menu button:hover:not(:disabled){color:var(--gp-accent-ink)}
403
- .gp-menu button:disabled{opacity:.4;cursor:default}
427
+ .gp-review-btn svg{width:13px;height:13px;flex:none}
428
+
429
+ /* --- changes -------------------------------------------------------------- */
430
+ .gp-changes{flex:1;min-height:90px;padding:2px 0 6px}
404
431
  .gp-section{flex:none}
405
- .gp-section-head{display:flex;align-items:center;gap:6px;padding:0 8px 0 10px;min-height:30px;cursor:pointer;user-select:none;transition:background .1s ease-out}
406
- .gp-section-head:hover{background:var(--gp-fill-4)}
407
- .gp-section-title{font-weight:600;font-size:11px;letter-spacing:.055em;text-transform:uppercase;color:var(--gp-label-3)}
408
- .gp-section-count{margin-left:2px;font-size:11px;color:var(--gp-label-3);font-variant-numeric:tabular-nums}
409
- .gp-chevron{width:11px;height:11px;flex:none;color:var(--gp-label-3);transition:transform .18s cubic-bezier(.32,.72,0,1)}
432
+ .gp-section-head{display:flex;align-items:center;gap:2px;padding:0 6px;height:30px}
433
+ .gp-section-head-graph{height:34px;padding:0 6px 0 4px;border-top:.5px solid var(--gp-hairline);border-bottom:.5px solid var(--gp-hairline);gap:2px}
434
+ .gp-section-toggle{display:flex;align-items:center;gap:6px;min-width:0;height:24px;padding:0 5px;border-radius:var(--gp-radius-s);cursor:pointer;user-select:none;color:inherit;background:transparent;transition:background .12s var(--gp-ease)}
435
+ .gp-section-toggle:hover{background:var(--gp-hover)}
436
+ .gp-section-toggle:focus-visible{outline:none;box-shadow:var(--gp-ring)}
437
+ .gp-section-title{font-size:10.5px;font-weight:650;letter-spacing:.045em;text-transform:uppercase;color:var(--gp-label-3);white-space:nowrap}
438
+ .gp-section-count{min-width:17px;height:15px;padding:0 5px;border-radius:999px;background:var(--gp-hover);color:var(--gp-label-3);font-size:10px;font-weight:600;line-height:15px;text-align:center;font-variant-numeric:tabular-nums}
439
+ .gp-chevron{width:11px;height:11px;flex:none;color:var(--gp-label-3);transition:transform .2s var(--gp-ease)}
410
440
  .gp-chevron.open{transform:rotate(90deg)}
411
- .gp-section-actions{margin-left:auto;display:flex;align-items:center;gap:2px}
412
- .gp-group-head{display:flex;align-items:center;gap:6px;padding:2px 8px 2px 22px;min-height:22px;color:var(--gp-label-3);font-size:11px;font-weight:500}
413
- .gp-group-head .gp-section-actions{gap:0}
414
- .gp-row{display:flex;align-items:center;gap:8px;min-height:30px;padding:0 6px 0 22px;cursor:pointer;position:relative;transition:background .1s ease-out}
415
- .gp-row:hover{background:var(--gp-fill-4)}
416
- .gp-row.selected{background:rgba(128,161,192,.18)}
417
- .gp-row .gp-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;letter-spacing:-.006em}
441
+ .gp-section-actions{position:relative;margin-left:auto;display:flex;align-items:center;gap:1px;flex:none}
442
+ /* The branch popover belongs to the header's action cluster, not to the chip:
443
+ anchored to the chip it would hang off the panel's left edge. */
444
+ .gp-section-actions .gp-menu-wrap{position:static}
445
+ /* A narrow pane gives the branch chip the room fetch would otherwise take;
446
+ sync already covers fetch + pull + push. */
447
+ @container (max-width: 300px){
448
+ .gp-section-head-graph .gp-btn-fetch{display:none}
449
+ }
450
+
451
+ .gp-row{display:flex;align-items:center;gap:8px;height:32px;margin:0 6px;padding:0 6px 0 8px;border-radius:7px;cursor:pointer;position:relative;transition:background .12s var(--gp-ease)}
452
+ .gp-row:hover{background:var(--gp-hover)}
453
+ .gp-row:active{background:var(--gp-press)}
454
+ .gp-row:focus-visible{outline:none;box-shadow:var(--gp-ring)}
455
+ .gp-row .gp-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;letter-spacing:-.01em}
418
456
  .gp-row .gp-name .gp-dir{color:var(--gp-label-3)}
419
- /* One chip per change kind: M pencil, A plus, U dashed plus, D minus, R arrow,
420
- C copy, T swap, CONFLICT alert. The chip carries the colour; the tooltip and
421
- aria-label carry the word. */
422
- .gp-change{flex:none;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:5.5px;background:var(--gp-fill-4);border:.5px solid transparent;color:var(--gp-label-2);transition:background .12s ease-out}
423
- .gp-change svg{width:12px;height:12px;flex:none}
424
- .gp-change.M{color:var(--gp-orange);background:rgba(255,159,10,.13);border-color:rgba(255,159,10,.3)}
425
- .gp-change.A{color:var(--gp-green);background:rgba(48,209,88,.13);border-color:rgba(48,209,88,.3)}
426
- .gp-change.U{color:var(--gp-green);background:rgba(48,209,88,.06);border:.5px dashed rgba(48,209,88,.55)}
427
- .gp-change.D{color:var(--gp-red);background:rgba(255,69,58,.13);border-color:rgba(255,69,58,.3)}
428
- .gp-change.R,.gp-change.C{color:var(--gp-blue);background:rgba(10,132,255,.14);border-color:rgba(10,132,255,.32)}
429
- .gp-change.T{color:var(--gp-purple);background:rgba(191,90,242,.14);border-color:rgba(191,90,242,.32)}
430
- .gp-change.CONFLICT{color:var(--gp-red);background:rgba(255,69,58,.2);border-color:rgba(255,69,58,.45)}
431
- .gp-ftype{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;color:var(--gp-label-3)}
432
- .gp-ftype svg{width:14px;height:14px;flex:none}
433
- .gp-ftype.code{color:#e0c07a}
434
- .gp-ftype.style{color:#6fc7e8}
435
- .gp-ftype.doc{color:#7fa6cf}
436
- .gp-ftype.image{color:#c08ed6}
437
- .gp-ftype.media{color:#e08ab0}
438
- .gp-ftype.font{color:#d8c98a}
439
- .gp-ftype.archive{color:#9aa3ad}
440
- .gp-ftype.lock{color:#a9a9b3}
441
- .gp-ftype.data{color:#8fcfa0}
442
- .gp-ftype.shell{color:#8fd39a}
443
- .gp-row-conflict .gp-change{color:var(--gp-red)}
444
- .gp-counts{display:flex;gap:5px;flex:none;font-size:10.5px;font-variant-numeric:tabular-nums;color:var(--gp-label-3);font-family:var(--gp-mono)}
457
+ .gp-row-conflict .gp-name{color:var(--gp-red)}
458
+ .gp-counts{display:flex;gap:6px;flex:none;font-size:10.5px;font-variant-numeric:tabular-nums;color:var(--gp-label-3);font-family:var(--gp-mono)}
445
459
  .gp-add{color:var(--gp-green)}
446
460
  .gp-del{color:var(--gp-red)}
447
461
  .gp-row-actions{display:none;align-items:center;gap:1px;flex:none}
448
462
  .gp-row:hover .gp-row-actions{display:flex}
449
463
  .gp-row:hover .gp-counts{display:none}
450
464
  .gp-row:hover .gp-change{display:none}
465
+
466
+ /* --- change-kind chips ---------------------------------------------------- */
467
+ .gp-change{flex:none;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:5.5px;background:var(--gp-hover);border:.5px solid transparent;color:var(--gp-label-2)}
468
+ .gp-change svg{width:12px;height:12px;flex:none}
469
+ .gp-change.M{color:var(--gp-orange);background:color-mix(in srgb,var(--gp-orange) 13%,transparent);border-color:color-mix(in srgb,var(--gp-orange) 28%,transparent)}
470
+ .gp-change.A{color:var(--gp-green);background:color-mix(in srgb,var(--gp-green) 13%,transparent);border-color:color-mix(in srgb,var(--gp-green) 28%,transparent)}
471
+ .gp-change.U{color:var(--gp-green);background:color-mix(in srgb,var(--gp-green) 6%,transparent);border:.5px dashed color-mix(in srgb,var(--gp-green) 52%,transparent)}
472
+ .gp-change.D{color:var(--gp-red);background:color-mix(in srgb,var(--gp-red) 13%,transparent);border-color:color-mix(in srgb,var(--gp-red) 28%,transparent)}
473
+ .gp-change.R,.gp-change.C{color:var(--gp-blue);background:color-mix(in srgb,var(--gp-blue) 13%,transparent);border-color:color-mix(in srgb,var(--gp-blue) 28%,transparent)}
474
+ .gp-change.T{color:var(--gp-purple);background:color-mix(in srgb,var(--gp-purple) 13%,transparent);border-color:color-mix(in srgb,var(--gp-purple) 28%,transparent)}
475
+ .gp-change.CONFLICT{color:var(--gp-red);background:color-mix(in srgb,var(--gp-red) 18%,transparent);border-color:color-mix(in srgb,var(--gp-red) 42%,transparent)}
476
+
477
+ /* --- file families -------------------------------------------------------- */
478
+ .gp-ftype{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;color:var(--gp-label-4)}
479
+ .gp-ftype svg{width:14px;height:14px;flex:none}
480
+ .gp-ftype.code{color:var(--gp-file-code)}
481
+ .gp-ftype.data{color:var(--gp-file-data)}
482
+ .gp-ftype.style{color:var(--gp-file-style)}
483
+ .gp-ftype.doc{color:var(--gp-file-doc)}
484
+ .gp-ftype.image{color:var(--gp-file-image)}
485
+ .gp-ftype.media{color:var(--gp-file-media)}
486
+ .gp-ftype.font{color:var(--gp-file-font)}
487
+ .gp-ftype.archive{color:var(--gp-file-archive)}
488
+ .gp-ftype.lock{color:var(--gp-file-lock)}
489
+ .gp-ftype.shell{color:var(--gp-file-shell)}
490
+
491
+ /* --- buttons -------------------------------------------------------------- */
492
+ .gp-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;height:24px;min-width:24px;padding:0 5px;border-radius:var(--gp-radius-s);border:none;background:transparent;color:var(--gp-label-3);font:inherit;font-size:11.5px;cursor:pointer;transition:background .12s var(--gp-ease),color .12s var(--gp-ease)}
493
+ .gp-btn:hover:not(:disabled){background:var(--gp-hover);color:var(--gp-label)}
494
+ .gp-btn:active:not(:disabled){background:var(--gp-press)}
495
+ .gp-btn:focus-visible{outline:none;box-shadow:var(--gp-ring)}
496
+ .gp-btn:disabled{opacity:.36;cursor:default}
497
+ .gp-btn svg{width:13px;height:13px;flex:none}
498
+ .gp-btn.active{background:var(--gp-accent-soft);color:var(--gp-accent)}
499
+ .gp-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--gp-red) 16%,transparent);color:var(--gp-red)}
500
+
501
+ .gp-primary{height:30px;padding:0 12px;border-radius:8px;border:none;background:var(--gp-accent);color:var(--gp-accent-ink);font:inherit;font-size:12.5px;font-weight:600;letter-spacing:-.01em;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:background .12s var(--gp-ease),transform .08s var(--gp-ease)}
502
+ .gp-primary:hover:not(:disabled){background:var(--gp-accent-hover)}
503
+ .gp-primary:active:not(:disabled){transform:scale(.99)}
504
+ .gp-primary:focus-visible{outline:none;box-shadow:var(--gp-ring),0 0 0 1px var(--gp-bg)}
505
+ .gp-primary:disabled{background:color-mix(in srgb,var(--gp-label) 5%,transparent);color:var(--gp-label-4);cursor:default}
506
+ .gp-primary svg{width:14px;height:14px;flex:none}
507
+
508
+ /* --- commit box ----------------------------------------------------------- */
509
+ .gp-compose{flex:none;display:flex;flex-direction:column;gap:8px;padding:12px 12px 10px;border-bottom:.5px solid var(--gp-hairline)}
510
+ .gp-input{box-sizing:border-box;width:100%;min-height:38px;max-height:140px;resize:none;field-sizing:content;padding:9px 10px;border-radius:10px;border:.5px solid var(--gp-hairline);background:color-mix(in srgb,var(--gp-label) 4%,transparent);color:var(--gp-label);font:inherit;font-size:12.5px;line-height:1.45;outline:none;transition:border-color .12s var(--gp-ease),box-shadow .12s var(--gp-ease),background .12s var(--gp-ease)}
511
+ .gp-input::placeholder{color:var(--gp-label-4)}
512
+ .gp-input:hover{border-color:var(--gp-separator)}
513
+ .gp-input:focus{border-color:color-mix(in srgb,var(--gp-accent) 60%,transparent);box-shadow:var(--gp-ring);background:color-mix(in srgb,var(--gp-label) 7%,transparent)}
514
+ .gp-input-wrap{position:relative;display:flex}
515
+ .gp-input-wrap .gp-input{padding-right:34px}
516
+ .gp-input-ai{position:absolute;top:6px;right:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;border-radius:var(--gp-radius-s);background:transparent;color:var(--gp-label-4);cursor:pointer;transition:background .12s var(--gp-ease),color .12s var(--gp-ease)}
517
+ .gp-input-ai:hover:not(:disabled){background:var(--gp-hover);color:var(--gp-label-2)}
518
+ .gp-input-ai:focus-visible{outline:none;box-shadow:var(--gp-ring)}
519
+ .gp-input-ai:disabled{opacity:.32;cursor:default}
520
+ .gp-input-ai svg{width:14px;height:14px;flex:none}
521
+ .gp-actions{display:flex;align-items:center;gap:6px}
522
+ .gp-actions .gp-primary{width:100%}
523
+ .gp-primary-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
524
+
525
+ /* --- branch chip + popover menu ------------------------------------------- */
526
+ .gp-menu-wrap{position:relative;display:inline-flex;min-width:0}
527
+ .gp-branch{display:inline-flex;align-items:center;gap:5px;min-width:0;max-width:132px;height:24px;padding:0 8px;border-radius:7px;background:var(--gp-hover);border:.5px solid var(--gp-hairline);color:var(--gp-label);font-family:var(--gp-mono);font-size:11px;letter-spacing:0;cursor:pointer;transition:background .12s var(--gp-ease),border-color .12s var(--gp-ease)}
528
+ .gp-branch:hover{background:var(--gp-press)}
529
+ .gp-branch:focus-visible{outline:none;box-shadow:var(--gp-ring)}
530
+ .gp-branch svg{width:11px;height:11px;flex:none;color:var(--gp-label-3)}
531
+ .gp-branch-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
532
+ .gp-branch-meta{display:flex;gap:3px;flex:none;font-size:9.5px;color:var(--gp-label-3);font-variant-numeric:tabular-nums}
533
+ .gp-branch-meta .up{color:var(--gp-green)}
534
+ .gp-branch-meta .down{color:var(--gp-orange)}
535
+ .gp-branch-waiting{cursor:default;color:var(--gp-label-3)}
536
+ .gp-menu{position:absolute;z-index:50;top:calc(100% + 6px);right:0;min-width:216px;max-height:min(60vh,340px);overflow:auto;padding:5px;border-radius:11px;background:color-mix(in srgb,var(--gp-surface-3) 90%,transparent);backdrop-filter:saturate(180%) blur(30px);-webkit-backdrop-filter:saturate(180%) blur(30px);border:.5px solid var(--gp-hairline);box-shadow:0 18px 44px rgba(0,0,0,.42),0 0 0 .5px rgba(0,0,0,.18);animation:gp-pop .14s var(--gp-ease);display:flex;flex-direction:column;gap:1px}
537
+ .gp-menu.up{top:auto;bottom:calc(100% + 6px);animation-name:gp-pop-up}
538
+ .gp-menu-title{padding:4px 8px 3px;font-size:9.5px;font-weight:650;letter-spacing:.05em;text-transform:uppercase;color:var(--gp-label-4)}
539
+ .gp-menu button{display:flex;width:100%;align-items:center;gap:8px;height:28px;padding:0 8px;border:none;border-radius:var(--gp-radius-s);background:transparent;color:var(--gp-label);font:inherit;font-size:12px;text-align:left;cursor:pointer;transition:background .1s var(--gp-ease),color .1s var(--gp-ease)}
540
+ .gp-menu button:hover:not(:disabled){background:var(--gp-accent);color:var(--gp-accent-ink)}
541
+ .gp-menu button:hover:not(:disabled) svg{color:currentColor}
542
+ .gp-menu button:disabled{color:var(--gp-label-3);cursor:default}
543
+ .gp-menu button svg{width:12px;height:12px;flex:none;color:var(--gp-label-3)}
544
+ .gp-menu button span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
545
+ .gp-menu-check{margin-left:auto;display:inline-flex;color:var(--gp-accent)}
546
+ .gp-menu-check svg{color:currentColor}
547
+
548
+ /* --- graph ---------------------------------------------------------------- */
549
+ .gp-resizer{flex:none;position:relative;height:10px;margin:-3px 0;cursor:row-resize;background:transparent;user-select:none;touch-action:none}
550
+ .gp-resizer:after{content:"";position:absolute;left:50%;top:50%;width:38px;height:3px;margin:-1.5px 0 0 -19px;border-radius:2px;background:var(--gp-separator);opacity:0;transition:opacity .15s var(--gp-ease)}
551
+ .gp-resizer:hover:after,.gp-resizer:active:after{opacity:1}
552
+ .gp-graph-panel{flex:none;display:flex;flex-direction:column;min-height:0;position:relative;overflow:visible}
553
+ .gp-graph-list{flex:1;min-height:0;padding:2px 0 6px}
451
554
  .gp-graph{position:relative}
452
- .gp-graph-row{display:flex;align-items:center;gap:8px;min-height:38px;padding:0 8px 0 0;cursor:pointer;position:relative;transition:background .1s ease-out}
453
- .gp-graph-row:hover{background:var(--gp-fill-4)}
454
- .gp-graph-row.selected{background:rgba(128,161,192,.18)}
455
- .gp-graph-canvas{flex:none;display:block;overflow:visible}
456
- .gp-graph-row:hover .gp-graph-canvas{filter:brightness(1.18)}
457
- .gp-graph-row.selected .gp-graph-canvas{filter:brightness(1.3) saturate(1.1)}
555
+ .gp-graph-row{display:flex;align-items:center;gap:8px;min-height:38px;margin:0 6px;padding:0 8px 0 2px;border-radius:8px;cursor:pointer;position:relative;transition:background .12s var(--gp-ease)}
556
+ .gp-graph-row:hover{background:var(--gp-hover)}
557
+ .gp-graph-row:focus-visible{outline:none;box-shadow:var(--gp-ring)}
558
+ .gp-graph-row.selected{background:var(--gp-accent-soft);box-shadow:inset 2px 0 0 var(--gp-accent)}
559
+ .gp-graph-canvas{flex:none;display:block;overflow:visible;transition:filter .15s var(--gp-ease)}
560
+ .gp-graph-row:hover .gp-graph-canvas{filter:brightness(1.12)}
561
+ .gp-graph-row.selected .gp-graph-canvas{filter:brightness(1.2) saturate(1.05)}
458
562
  .gp-graph-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
459
- .gp-graph-subject{display:flex;align-items:center;gap:6px;min-width:0}
460
- .gp-graph-subject .gp-subject-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px}
461
- .gp-ref{flex:none;max-width:132px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--gp-fill-3);color:var(--gp-label-2);font-weight:500}
462
- .gp-ref.head{background:rgba(10,132,255,.24);color:#9dc0ff}
463
- .gp-ref.tag{background:rgba(255,159,10,.2);color:#ffc477}
464
- .gp-ref.remote{background:rgba(191,90,242,.2);color:#d7a8ff}
465
- .gp-graph-meta{display:flex;align-items:center;gap:7px;font-size:10.5px;color:var(--gp-label-3)}
466
- .gp-graph-meta .gp-hash{font-family:var(--gp-mono)}
467
- .gp-detail{padding:8px 10px 10px 22px;border-bottom:.5px solid var(--gp-separator);background:rgba(120,120,128,.08)}
468
- .gp-detail-body{white-space:pre-wrap;word-break:break-word;font-size:11.5px;color:var(--gp-label-2);margin:5px 0 7px}
469
- .gp-kv{display:flex;gap:7px;font-size:11px;color:var(--gp-label-3)}
470
- .gp-kv b{font-weight:590;color:var(--gp-label-2)}
471
- .gp-detail-actions{display:flex;gap:2px;margin-top:7px}
472
- .gp-detail-files{margin-top:7px;display:flex;flex-direction:column;gap:2px;max-height:168px;overflow:auto}
473
- .gp-detail-file{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--gp-label-2)}
563
+ .gp-graph-subject{display:flex;align-items:center;gap:5px;min-width:0}
564
+ .gp-graph-subject .gp-subject-text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;letter-spacing:-.01em}
565
+ .gp-ref{flex:none;max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;height:17px;padding:0 6px;border-radius:999px;background:var(--gp-hover);color:var(--gp-label-2);font-size:9.5px;font-weight:600;line-height:17px;letter-spacing:.01em}
566
+ .gp-ref.head{background:color-mix(in srgb,var(--gp-accent) 24%,transparent);color:var(--gp-accent)}
567
+ .gp-ref.tag{background:color-mix(in srgb,var(--gp-orange) 20%,transparent);color:var(--gp-orange)}
568
+ .gp-ref.remote{background:color-mix(in srgb,var(--gp-purple) 20%,transparent);color:var(--gp-purple)}
569
+ .gp-graph-meta{display:flex;align-items:center;gap:6px;min-width:0;overflow:hidden;white-space:nowrap;font-size:10.5px;color:var(--gp-label-4);font-variant-numeric:tabular-nums}
570
+ .gp-graph-meta > span{flex:none;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
571
+ .gp-graph-meta > span:not(.gp-hash){flex:0 1 auto}
572
+ .gp-graph-meta > span + span:before{content:"·";margin-right:6px;color:var(--gp-label-4)}
573
+ .gp-graph-meta .gp-hash{font-family:var(--gp-mono);font-size:10px;color:var(--gp-label-3);background:var(--gp-hover);border-radius:4px;padding:0 4px;line-height:15px}
574
+ .gp-detail{margin:2px 6px 8px;padding:10px 12px;border-radius:var(--gp-radius-m);background:color-mix(in srgb,var(--gp-label) 4%,transparent);border:.5px solid var(--gp-hairline);animation:gp-fade .16s var(--gp-ease)}
575
+ .gp-detail-body{white-space:pre-wrap;word-break:break-word;font-size:11.5px;color:var(--gp-label-2);margin:6px 0 8px}
576
+ .gp-kv{display:flex;gap:8px;font-size:11px;color:var(--gp-label-3);line-height:1.7}
577
+ .gp-kv b{font-weight:600;color:var(--gp-label-2)}
578
+ .gp-detail-actions{display:flex;gap:1px;margin:6px 0 0 -5px}
579
+ .gp-detail-files{margin-top:8px;padding-top:8px;border-top:.5px solid var(--gp-hairline);display:flex;flex-direction:column;gap:1px;max-height:168px;overflow:auto}
580
+ .gp-detail-file{display:flex;align-items:center;gap:7px;height:24px;padding:0 4px;border-radius:5px;font-size:11px;color:var(--gp-label-2)}
581
+ .gp-detail-file:hover{background:var(--gp-hover)}
474
582
  .gp-detail-file .gp-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
475
- .gp-note{padding:12px;color:var(--gp-label-3);font-size:11.5px;text-align:center}
476
- .gp-error{margin:8px;padding:8px 10px;border-radius:10px;background:rgba(255,69,58,.12);border:.5px solid rgba(255,69,58,.28);color:var(--gp-label);font-size:11.5px;display:flex;gap:8px;align-items:center}
477
- .gp-error span{flex:1;min-width:0;word-break:break-word;white-space:pre-wrap}
478
- /* Confirmation is an in-panel overlay anchored to the bottom edge: it used to be
479
- a sticky row at the end of the column, which landed outside the visible pane
480
- whenever the graph held its height. */
481
- .gp-confirm-scrim{position:absolute;inset:0;z-index:60;display:flex;align-items:flex-end;padding:8px;background:rgba(12,12,14,.32)}
482
- .gp-confirm{flex:1;padding:9px 10px;border-radius:10px;background:rgba(44,44,48,.96);backdrop-filter:saturate(180%) blur(24px);-webkit-backdrop-filter:saturate(180%) blur(24px);border:.5px solid rgba(255,255,255,.12);box-shadow:0 12px 32px rgba(0,0,0,.5);font-size:11.5px;display:flex;gap:8px;align-items:center}
483
- .gp-confirm span{flex:1;min-width:0;line-height:1.45}
484
- .gp-confirm .gp-primary{height:24px;padding:0 10px;font-size:11.5px;flex:none}
583
+
584
+ /* --- status note / error / toast ------------------------------------------ */
585
+ .gp-note{padding:18px 14px;color:var(--gp-label-3);font-size:11.5px;text-align:center;line-height:1.5}
586
+ .gp-error{margin:8px 12px 0;padding:9px 10px;border-radius:10px;background:color-mix(in srgb,var(--gp-red) 10%,transparent);border:.5px solid color-mix(in srgb,var(--gp-red) 24%,transparent);color:var(--gp-label);font-size:11.5px;display:flex;gap:8px;align-items:flex-start;animation:gp-fade .16s var(--gp-ease)}
587
+ .gp-error-icon{flex:none;display:inline-flex;margin-top:1px;color:var(--gp-red)}
588
+ .gp-error-icon svg{width:13px;height:13px}
589
+ .gp-error span.gp-error-text{flex:1;min-width:0;word-break:break-word;white-space:pre-wrap}
590
+ .gp-toast{margin:8px 12px 0;padding:8px 10px;border-radius:9px;background:var(--gp-hover);color:var(--gp-label-2);font-size:11.5px;display:flex;gap:7px;align-items:center;animation:gp-fade .16s var(--gp-ease)}
591
+ .gp-toast svg{width:13px;height:13px;flex:none;color:var(--gp-green)}
592
+
593
+ /* --- confirm overlay ------------------------------------------------------ */
594
+ .gp-confirm-scrim{position:absolute;inset:0;z-index:60;display:flex;align-items:flex-end;padding:10px;background:color-mix(in srgb,#000 36%,transparent);animation:gp-fade .16s var(--gp-ease)}
595
+ .gp-confirm{flex:1;padding:10px 12px;border-radius:var(--gp-radius-l);background:color-mix(in srgb,var(--gp-surface-3) 92%,transparent);backdrop-filter:saturate(180%) blur(28px);-webkit-backdrop-filter:saturate(180%) blur(28px);border:.5px solid var(--gp-hairline);box-shadow:0 18px 44px rgba(0,0,0,.45),0 0 0 .5px rgba(0,0,0,.2);font-size:11.5px;display:flex;gap:8px;align-items:center;animation:gp-rise .2s var(--gp-ease)}
596
+ .gp-confirm span{flex:1;min-width:0;line-height:1.5}
597
+ .gp-confirm .gp-primary{height:26px;padding:0 10px;font-size:11.5px;flex:none}
598
+
599
+ /* --- diff page ------------------------------------------------------------ */
485
600
  .gp-diff{display:flex;flex-direction:column;height:100%;min-height:0;font-size:11.5px}
486
- .gp-diff-head{display:flex;align-items:center;gap:7px;padding:0 10px;min-height:34px;flex:none;border-bottom:.5px solid var(--gp-separator)}
601
+ .gp-diff-head{display:flex;align-items:center;gap:8px;padding:0 12px;height:40px;flex:none;border-bottom:.5px solid var(--gp-hairline)}
487
602
  .gp-diff-head .gp-path{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--gp-mono);font-size:11.5px}
488
- .gp-diff-body{flex:1;min-height:0;overflow:auto;font-family:var(--gp-mono);font-size:11.5px;line-height:18px;scrollbar-width:thin}
489
- .gp-diff-line{display:flex;min-width:max-content;white-space:pre}
490
- .gp-diff-line .gp-ln{flex:none;width:42px;padding-right:8px;text-align:right;color:var(--gp-label-4);user-select:none;font-variant-numeric:tabular-nums}
491
- .gp-diff-line .gp-sign{flex:none;width:12px;text-align:center}
492
- .gp-diff-line .gp-code{flex:1;padding-right:14px;white-space:pre}
493
- .gp-diff-line.add{background:rgba(48,209,88,.13)}
603
+ .gp-diff-body{flex:1;min-height:0;font-family:var(--gp-mono);font-size:11.5px;line-height:18px}
604
+ .gp-diff-line{display:flex;box-sizing:border-box;width:max-content;min-width:100%;white-space:pre}
605
+ .gp-diff-line.meta,.gp-diff-line.hunk{padding-left:10px}
606
+ .gp-diff-line.meta .gp-ln,.gp-diff-line.hunk .gp-ln{width:0;padding:0}
607
+ .gp-diff-line.meta .gp-sign,.gp-diff-line.hunk .gp-sign{width:0}
608
+ .gp-diff-line .gp-ln{flex:none;width:40px;padding-right:8px;text-align:right;color:var(--gp-label-4);user-select:none;font-variant-numeric:tabular-nums;font-size:10.5px}
609
+ .gp-diff-line .gp-sign{flex:none;width:14px;text-align:center}
610
+ .gp-diff-line .gp-code{flex:1;padding-right:16px;white-space:pre}
611
+ .gp-diff-line.add{background:var(--gp-code-add)}
494
612
  .gp-diff-line.add .gp-sign{color:var(--gp-green)}
495
- .gp-diff-line.del{background:rgba(255,69,58,.13)}
613
+ .gp-diff-line.del{background:var(--gp-code-del)}
496
614
  .gp-diff-line.del .gp-sign{color:var(--gp-red)}
497
- .gp-diff-line.hunk{background:var(--gp-fill-4);color:var(--gp-label-3)}
498
- .gp-diff-line.meta{color:var(--gp-label-3)}
499
- .gp-diff-line .gp-prefix{color:var(--gp-label-3)}
615
+ .gp-diff-line.hunk{background:var(--gp-surface-2);color:var(--gp-label-3);box-shadow:inset 2px 0 0 color-mix(in srgb,var(--gp-blue) 45%,transparent)}
616
+ .gp-diff-line.meta{color:var(--gp-label-4)}
617
+
618
+ /* --- misc ----------------------------------------------------------------- */
619
+ .gp-tag{display:inline-flex;align-items:center;height:17px;padding:0 6px;border-radius:999px;background:var(--gp-hover);color:var(--gp-label-3);font-size:9.5px;font-weight:600}
500
620
  .gp-spinner{display:inline-block;width:11px;height:11px;border:1.5px solid currentColor;border-right-color:transparent;border-radius:50%;animation:gp-spin .7s linear infinite}
501
621
  @keyframes gp-spin{to{transform:rotate(360deg)}}
502
-
622
+ @keyframes gp-pop{from{opacity:0;transform:scale(.97) translateY(-3px)}to{opacity:1;transform:none}}
623
+ @keyframes gp-pop-up{from{opacity:0;transform:scale(.97) translateY(3px)}to{opacity:1;transform:none}}
624
+ @keyframes gp-rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
625
+ @keyframes gp-fade{from{opacity:0}to{opacity:1}}
503
626
  `
504
627
 
505
628
  /* ------------------------------------------------------------------ */
@@ -944,6 +1067,27 @@ window.__ModuleLoader__.load({
944
1067
 
945
1068
  const ChevronIcon = (props) => h('svg', { ...iconProps, width: 12, height: 12, className: props?.className }, h('path', { d: 'M6 3.5 10.5 8 6 12.5' }))
946
1069
  const RefreshIcon = () => h('svg', iconProps, h('path', { d: 'M13.5 8a5.5 5.5 0 1 1-1.9-4.2' }), h('path', { d: 'M13.5 2v3.2h-3.2' }))
1070
+ /** Two arrows around a circle: fetch + pull + push, one verb. */
1071
+ const SyncIcon = () =>
1072
+ h(
1073
+ 'svg',
1074
+ glyphProps,
1075
+ h('path', { d: 'M2.6 8a5.4 5.4 0 0 1 9.2-3.8' }),
1076
+ h('path', { d: 'M13.4 8a5.4 5.4 0 0 1-9.2 3.8' }),
1077
+ h('path', { d: 'M12.4 1.6v2.9H9.5' }),
1078
+ h('path', { d: 'M3.6 14.4v-2.9h2.9' }),
1079
+ )
1080
+ /** The graph's refs filter: a lane that forks. */
1081
+ const GraphRefsIcon = () =>
1082
+ h(
1083
+ 'svg',
1084
+ glyphProps,
1085
+ h('circle', { cx: 4, cy: 3.4, r: 1.5 }),
1086
+ h('circle', { cx: 4, cy: 12.6, r: 1.5 }),
1087
+ h('circle', { cx: 11.8, cy: 8, r: 1.5 }),
1088
+ h('path', { d: 'M4 4.9v6.2' }),
1089
+ h('path', { d: 'M5.3 7.1c2.5 0 3.5.4 5 1' }),
1090
+ )
947
1091
  const PlusIcon = () => h('svg', iconProps, h('path', { d: 'M8 3v10M3 8h10' }))
948
1092
  const MinusIcon = () => h('svg', iconProps, h('path', { d: 'M3 8h10' }))
949
1093
  const UndoIcon = () => h('svg', iconProps, h('path', { d: 'M3 8a5 5 0 1 1 1.6 3.7' }), h('path', { d: 'M3 3.6V8h4.4' }))
@@ -1224,14 +1368,15 @@ window.__ModuleLoader__.load({
1224
1368
  /* ------------------------------------------------------------------ */
1225
1369
 
1226
1370
  /** A ghost icon button with an accessible label. */
1227
- function IconButton({ label, onClick, disabled, danger, children }) {
1371
+ function IconButton({ label, onClick, disabled, danger, active, className, children }) {
1228
1372
  return h(
1229
1373
  'button',
1230
1374
  {
1231
1375
  type: 'button',
1232
- className: `gp-btn${danger === true ? ' gp-btn-danger' : ''}`,
1376
+ className: `gp-btn${danger === true ? ' gp-btn-danger' : ''}${active === true ? ' active' : ''}${typeof className === 'string' ? ` ${className}` : ''}`,
1233
1377
  title: label,
1234
1378
  'aria-label': label,
1379
+ 'aria-pressed': active === undefined ? undefined : active === true,
1235
1380
  disabled: disabled === true,
1236
1381
  onClick: (event) => {
1237
1382
  event.stopPropagation()
@@ -1242,15 +1387,40 @@ window.__ModuleLoader__.load({
1242
1387
  )
1243
1388
  }
1244
1389
 
1245
- /** One collapsible section header. */
1246
- function SectionHeader({ title, count, expanded, onToggle, actions, t }) {
1390
+ /**
1391
+ * One collapsible section header.
1392
+ *
1393
+ * The collapse hit area is the label cluster only (chevron, title, count),
1394
+ * not the whole row: the right side now hosts real controls — the branch
1395
+ * menu and the repo verbs on the graph header — and a full-row toggle
1396
+ * would fire when the user aimed at them.
1397
+ */
1398
+ function SectionHeader({ title, count, expanded, onToggle, actions, t, variant }) {
1399
+ const label = expanded === true ? t('changes.collapse') : t('changes.expand')
1247
1400
  return h(
1248
1401
  'div',
1249
- { className: 'gp-section-head', onClick: onToggle, role: 'button', tabIndex: 0, onKeyDown: (event) => (event.key === 'Enter' || event.key === ' ' ? onToggle?.() : undefined) },
1250
- h(ChevronIcon, { className: `gp-chevron${expanded ? ' open' : ''}` }),
1251
- h('span', { className: 'gp-section-title' }, title),
1252
- count === undefined ? null : h('span', { className: 'gp-section-count' }, String(count)),
1253
- h('div', { className: 'gp-section-actions', onClick: (event) => event.stopPropagation() }, actions),
1402
+ { className: `gp-section-head${variant === 'graph' ? ' gp-section-head-graph' : ''}` },
1403
+ h(
1404
+ 'div',
1405
+ {
1406
+ className: 'gp-section-toggle',
1407
+ role: 'button',
1408
+ tabIndex: 0,
1409
+ 'aria-expanded': expanded === true,
1410
+ title: label,
1411
+ onClick: onToggle,
1412
+ onKeyDown: (event) => {
1413
+ if (event.key === 'Enter' || event.key === ' ') {
1414
+ event.preventDefault()
1415
+ onToggle?.()
1416
+ }
1417
+ },
1418
+ },
1419
+ h(ChevronIcon, { className: `gp-chevron${expanded ? ' open' : ''}` }),
1420
+ h('span', { className: 'gp-section-title' }, title),
1421
+ count === undefined ? null : h('span', { className: 'gp-section-count' }, String(count)),
1422
+ ),
1423
+ h('div', { className: 'gp-section-actions' }, actions),
1254
1424
  )
1255
1425
  }
1256
1426
 
@@ -1279,7 +1449,12 @@ window.__ModuleLoader__.load({
1279
1449
  h(FileTypeIcon, { path: entry.path }),
1280
1450
  h('span', { className: 'gp-name' }, dir === '' ? null : h('span', { className: 'gp-dir' }, dir), name),
1281
1451
  showCounts
1282
- ? h('span', { className: 'gp-counts' }, h('span', { className: 'gp-add' }, `+${entry.additions}`), h('span', { className: 'gp-del' }, `-${entry.deletions}`))
1452
+ ? h(
1453
+ 'span',
1454
+ { className: 'gp-counts' },
1455
+ entry.additions > 0 ? h('span', { className: 'gp-add' }, `+${entry.additions}`) : null,
1456
+ entry.deletions > 0 ? h('span', { className: 'gp-del' }, `-${entry.deletions}`) : null,
1457
+ )
1283
1458
  : null,
1284
1459
  h(ChangeBadge, { entry, t, group }),
1285
1460
  h(
@@ -1855,6 +2030,13 @@ window.__ModuleLoader__.load({
1855
2030
 
1856
2031
  const sectionToggle = (name) => () => setExpanded((current) => ({ ...current, [name]: current[name] !== true }))
1857
2032
 
2033
+ /** One refresh verb for the whole pane: status and history together. */
2034
+ const refreshAll = useCallback(() => {
2035
+ setStatusVersion((value2) => value2 + 1)
2036
+ setGraphVersion((value2) => value2 + 1)
2037
+ status.refresh?.(true)
2038
+ }, [status])
2039
+
1858
2040
  if (cwd === undefined) {
1859
2041
  return h('div', { className: 'gp-root' }, h('div', { className: 'gp-note' }, t('empty.noRepo')))
1860
2042
  }
@@ -1874,35 +2056,16 @@ window.__ModuleLoader__.load({
1874
2056
  return h(
1875
2057
  'div',
1876
2058
  { className: 'gp-root' },
1877
- h(
1878
- 'div',
1879
- { className: 'gp-toolbar' },
1880
- repo === undefined
1881
- ? h('span', { className: 'gp-branch' }, h('span', { className: 'gp-spinner' }), h('span', null, t('empty.loading')))
1882
- : h(
1883
- 'div',
1884
- { className: 'gp-menu-wrap' },
1885
- h(BranchMenu, { t, repo, busy, onCheckout: (name) => run('checkout', { name }) }),
1886
- ),
1887
- repo !== undefined && repo.detached === true ? h('span', { className: 'gp-tag' }, t('branch.detached')) : null,
1888
- repo !== undefined && repo.ahead > 0 ? h('span', { className: 'gp-tag' }, fill(t('branch.ahead'), { count: repo.ahead })) : null,
1889
- repo !== undefined && repo.behind > 0 ? h('span', { className: 'gp-tag' }, fill(t('branch.behind'), { count: repo.behind })) : null,
1890
- h('span', { className: 'gp-spacer' }),
1891
- repo === undefined
1892
- ? null
1893
- : h(IconButton, { label: t('status.fetch'), disabled: busy, onClick: () => run('network', { mode: 'fetch' }, { graph: false }) }, h(CloudIcon, null)),
1894
- repo === undefined ? null : h(IconButton, { label: t('status.sync'), disabled: busy, onClick: () => run('network', { mode: 'sync' }, { graph: false }) }, h(RefreshIcon, null)),
1895
- h(IconButton, { label: t('status.refresh'), disabled: busy, onClick: () => status.refresh?.(true) }, h(RefreshIcon, null)),
1896
- ),
1897
2059
  error === undefined
1898
2060
  ? null
1899
2061
  : h(
1900
2062
  'div',
1901
- { className: 'gp-error' },
1902
- h('span', null, `${t('error.title')}: ${error.message}`),
2063
+ { className: 'gp-error', role: 'alert' },
2064
+ h('span', { className: 'gp-error-icon' }, h(ChangeConflictIcon, null)),
2065
+ h('span', { className: 'gp-error-text' }, `${t('error.title')}:${error.message}`),
1903
2066
  h(IconButton, { label: t('error.dismiss'), onClick: () => setError(undefined) }, h('span', { 'aria-hidden': 'true' }, '✕')),
1904
2067
  ),
1905
- notice === undefined ? null : h('div', { className: 'gp-note' }, notice),
2068
+ notice === undefined ? null : h('div', { className: 'gp-toast', role: 'status' }, h(CheckIcon, null), h('span', null, notice)),
1906
2069
  h(CommitBox, {
1907
2070
  t,
1908
2071
  status: repo,
@@ -1912,7 +2075,9 @@ window.__ModuleLoader__.load({
1912
2075
  h(
1913
2076
  'div',
1914
2077
  { className: 'gp-changes' },
1915
- ...sections.map((section) =>
2078
+ repo === undefined
2079
+ ? h('div', { className: 'gp-note' }, t('empty.loading'))
2080
+ : sections.map((section) =>
1916
2081
  h(
1917
2082
  'div',
1918
2083
  { className: 'gp-section', key: section.id },
@@ -1967,7 +2132,7 @@ window.__ModuleLoader__.load({
1967
2132
  className: 'gp-review-btn',
1968
2133
  disabled: busy || (repo?.staged?.length ?? 0) + (repo?.changes?.length ?? 0) + (repo?.untracked?.length ?? 0) === 0,
1969
2134
  onClick: findIssues,
1970
- }, t('review.find')),
2135
+ }, h(SparkleIcon, null), t('review.find')),
1971
2136
  ),
1972
2137
  expanded.graph === false
1973
2138
  ? null
@@ -1984,91 +2149,127 @@ window.__ModuleLoader__.load({
1984
2149
  className: 'gp-graph-panel',
1985
2150
  style: expanded.graph === false ? null : { height: `${graphHeight}px` },
1986
2151
  },
1987
- h(
1988
- 'div',
1989
- { className: 'gp-section' },
1990
- h(SectionHeader, {
1991
- t,
1992
- title: t('graph.title'),
1993
- count: graphRows.rows.length,
1994
- expanded: expanded.graph,
1995
- onToggle: sectionToggle('graph'),
1996
- actions: [
1997
- h(IconButton, { key: 'all', label: graphAll ? t('graph.all') : t('graph.head'), onClick: () => setGraphAll((value2) => !value2) }, h(BranchIcon, { size: 12 })),
1998
- h(IconButton, { key: 'refresh', label: t('status.refresh'), onClick: () => setGraphVersion((value2) => value2 + 1) }, h(RefreshIcon, null)),
1999
- ],
2000
- }),
2001
- expanded.graph !== true
2002
- ? null
2003
- : graph.loading === true && graphRows.rows.length === 0
2004
- ? h('div', { className: 'gp-note' }, t('graph.loading'))
2005
- : graphRows.rows.length === 0
2006
- ? h('div', { className: 'gp-note' }, t('graph.empty'))
2007
- : h(
2008
- 'div',
2009
- { className: 'gp-graph' },
2010
- graphRows.rows.map((row) =>
2011
- h(
2012
- React.Fragment,
2013
- { key: row.commit.hash },
2152
+ h(SectionHeader, {
2153
+ t,
2154
+ variant: 'graph',
2155
+ title: t('graph.title'),
2156
+ count: graphRows.rows.length,
2157
+ expanded: expanded.graph,
2158
+ onToggle: sectionToggle('graph'),
2159
+ actions: [
2160
+ repo === undefined
2161
+ ? h('span', { className: 'gp-branch gp-branch-waiting', key: 'branch' }, h('span', { className: 'gp-spinner' }), h('span', { className: 'gp-branch-name' }, t('empty.loading')))
2162
+ : h(BranchMenu, {
2163
+ key: 'branch',
2164
+ t,
2165
+ repo,
2166
+ busy,
2167
+ placement: 'up',
2168
+ onCheckout: (name) => run('checkout', { name }),
2169
+ }),
2170
+ h(
2171
+ IconButton,
2172
+ {
2173
+ key: 'refs',
2174
+ label: graphAll ? t('graph.head') : t('graph.all'),
2175
+ active: graphAll,
2176
+ onClick: () => setGraphAll((value2) => !value2),
2177
+ },
2178
+ h(GraphRefsIcon, null),
2179
+ ),
2180
+ repo === undefined
2181
+ ? null
2182
+ : h(
2183
+ IconButton,
2184
+ {
2185
+ key: 'fetch',
2186
+ label: t('status.fetch'),
2187
+ className: 'gp-btn-fetch',
2188
+ disabled: busy,
2189
+ onClick: () => run('network', { mode: 'fetch' }, { graph: false }),
2190
+ },
2191
+ h(CloudIcon, null),
2192
+ ),
2193
+ repo === undefined
2194
+ ? null
2195
+ : h(IconButton, { key: 'sync', label: t('status.sync'), disabled: busy, onClick: () => run('network', { mode: 'sync' }, { graph: false }) }, h(SyncIcon, null)),
2196
+ h(IconButton, { key: 'refresh', label: t('status.refresh'), disabled: busy, onClick: refreshAll }, h(RefreshIcon, null)),
2197
+ ],
2198
+ }),
2199
+ expanded.graph !== true
2200
+ ? null
2201
+ : h(
2202
+ 'div',
2203
+ { className: 'gp-graph-list' },
2204
+ graph.loading === true && graphRows.rows.length === 0
2205
+ ? h('div', { className: 'gp-note' }, t('graph.loading'))
2206
+ : graphRows.rows.length === 0
2207
+ ? h('div', { className: 'gp-note' }, t('graph.empty'))
2208
+ : h(
2209
+ 'div',
2210
+ { className: 'gp-graph' },
2211
+ graphRows.rows.map((row) =>
2014
2212
  h(
2015
- 'div',
2016
- {
2017
- className: `gp-graph-row${selected === row.commit.hash ? ' selected' : ''}`,
2018
- onClick: () => selectCommit(row.commit),
2019
- role: 'button',
2020
- tabIndex: 0,
2021
- onKeyDown: (event) => (event.key === 'Enter' ? selectCommit(row.commit) : undefined),
2022
- },
2023
- h(GraphCanvas, { row, laneCount: graphRows.laneCount }),
2213
+ React.Fragment,
2214
+ { key: row.commit.hash },
2024
2215
  h(
2025
2216
  'div',
2026
- { className: 'gp-graph-main' },
2217
+ {
2218
+ className: `gp-graph-row${selected === row.commit.hash ? ' selected' : ''}`,
2219
+ onClick: () => selectCommit(row.commit),
2220
+ role: 'button',
2221
+ tabIndex: 0,
2222
+ onKeyDown: (event) => (event.key === 'Enter' ? selectCommit(row.commit) : undefined),
2223
+ },
2224
+ h(GraphCanvas, { row, laneCount: graphRows.laneCount }),
2027
2225
  h(
2028
2226
  'div',
2029
- { className: 'gp-graph-subject' },
2030
- h(Refs, { refs: row.commit.refs, t }),
2031
- h('span', { className: 'gp-subject-text', title: row.commit.subject }, row.commit.subject === '' ? '(无提交说明)' : row.commit.subject),
2032
- ),
2033
- h(
2034
- 'div',
2035
- { className: 'gp-graph-meta' },
2036
- h('span', { className: 'gp-hash' }, row.commit.short),
2037
- h('span', null, row.commit.author),
2038
- h('span', null, relativeTime(row.commit.at)),
2227
+ { className: 'gp-graph-main' },
2228
+ h(
2229
+ 'div',
2230
+ { className: 'gp-graph-subject' },
2231
+ h(Refs, { refs: row.commit.refs, t }),
2232
+ h('span', { className: 'gp-subject-text', title: row.commit.subject }, row.commit.subject === '' ? '(无提交说明)' : row.commit.subject),
2233
+ ),
2234
+ h(
2235
+ 'div',
2236
+ { className: 'gp-graph-meta' },
2237
+ h('span', { className: 'gp-hash' }, row.commit.short),
2238
+ h('span', null, row.commit.author),
2239
+ h('span', null, relativeTime(row.commit.at)),
2240
+ ),
2039
2241
  ),
2040
2242
  ),
2243
+ selected === row.commit.hash
2244
+ ? h(CommitDetail, {
2245
+ t,
2246
+ detail,
2247
+ loading: detailLoading,
2248
+ copied,
2249
+ onOpenDiff: (commit) => tabActions?.openTab(DIFF_KIND, { params: { hash: commit.hash, cwd: repo?.cwd } }),
2250
+ onCopy: (commit) => {
2251
+ try {
2252
+ navigator.clipboard?.writeText?.(commit.hash)
2253
+ } catch {}
2254
+ setCopied(true)
2255
+ setTimeout(() => setCopied(false), 1600)
2256
+ },
2257
+ onCheckout:
2258
+ row.commit.refs.some((ref) => ref.kind === 'branch' || ref.kind === 'head')
2259
+ ? () => {
2260
+ const ref = row.commit.refs.find((entry) => entry.kind === 'branch')
2261
+ if (ref !== undefined) run('checkout', { name: ref.name })
2262
+ }
2263
+ : undefined,
2264
+ })
2265
+ : null,
2041
2266
  ),
2042
- selected === row.commit.hash
2043
- ? h(CommitDetail, {
2044
- t,
2045
- detail,
2046
- loading: detailLoading,
2047
- copied,
2048
- onOpenDiff: (commit) => tabActions?.openTab(DIFF_KIND, { params: { hash: commit.hash, cwd: repo?.cwd } }),
2049
- onCopy: (commit) => {
2050
- try {
2051
- navigator.clipboard?.writeText?.(commit.hash)
2052
- } catch {}
2053
- setCopied(true)
2054
- setTimeout(() => setCopied(false), 1600)
2055
- },
2056
- onCheckout:
2057
- row.commit.refs.some((ref) => ref.kind === 'branch' || ref.kind === 'head')
2058
- ? () => {
2059
- const ref = row.commit.refs.find((entry) => entry.kind === 'branch')
2060
- if (ref !== undefined) run('checkout', { name: ref.name })
2061
- }
2062
- : undefined,
2063
- })
2064
- : null,
2065
2267
  ),
2268
+ graph.value?.truncated === true ? h('div', { className: 'gp-note' }, fill(t('graph.truncated'), { count: graphRows.rows.length })) : null,
2066
2269
  ),
2067
- graph.value?.truncated === true ? h('div', { className: 'gp-note' }, fill(t('graph.truncated'), { count: graphRows.rows.length })) : null,
2068
- ),
2069
- ),
2270
+ ),
2070
2271
  ),
2071
- confirming === undefined
2272
+ confirming === undefined
2072
2273
  ? null
2073
2274
  : h(ConfirmBar, {
2074
2275
  t,
@@ -2154,11 +2355,37 @@ window.__ModuleLoader__.load({
2154
2355
  )
2155
2356
  }
2156
2357
 
2157
- /** The branch chip and its checkout menu. */
2158
- function BranchMenu({ t, repo, busy, onCheckout }) {
2358
+ /**
2359
+ * What the branch chip says: the checked-out name (or the short hash when
2360
+ * detached) plus the divergence the old toolbar used to show as three
2361
+ * separate tags.
2362
+ * @param {object} repo - status payload.
2363
+ * @returns {{label: string, detached: boolean, ahead: number, behind: number}} chip facts.
2364
+ */
2365
+ function branchSummaryOf(repo) {
2366
+ const detached = repo?.detached === true
2367
+ const raw = detached ? String(repo?.oid ?? '') : String(repo?.branch ?? '')
2368
+ const label = detached ? raw.slice(0, 7) || 'HEAD' : raw
2369
+ return {
2370
+ label,
2371
+ detached,
2372
+ ahead: Number(repo?.ahead) || 0,
2373
+ behind: Number(repo?.behind) || 0,
2374
+ }
2375
+ }
2376
+
2377
+ /**
2378
+ * The branch chip and its checkout menu.
2379
+ *
2380
+ * It lives in the graph header now, which sits at the bottom of the pane,
2381
+ * so the menu opens upward (`placement="up"`); a downward popover would be
2382
+ * clipped by the sidebar.
2383
+ */
2384
+ function BranchMenu({ t, repo, busy, onCheckout, placement }) {
2159
2385
  const [open, setOpen] = useState(false)
2160
2386
  const [branches, setBranches] = useState(undefined)
2161
2387
  const ref = useRef(null)
2388
+ const summary = branchSummaryOf(repo)
2162
2389
 
2163
2390
  useEffect(() => {
2164
2391
  if (!open || branches !== undefined) return undefined
@@ -2173,24 +2400,54 @@ window.__ModuleLoader__.load({
2173
2400
  const onDown = (event) => {
2174
2401
  if (ref.current !== null && !ref.current.contains(event.target)) setOpen(false)
2175
2402
  }
2403
+ const onKey = (event) => {
2404
+ if (event.key === 'Escape') setOpen(false)
2405
+ }
2176
2406
  document.addEventListener('mousedown', onDown)
2177
- return () => document.removeEventListener('mousedown', onDown)
2407
+ document.addEventListener('keydown', onKey)
2408
+ return () => {
2409
+ document.removeEventListener('mousedown', onDown)
2410
+ document.removeEventListener('keydown', onKey)
2411
+ }
2178
2412
  }, [open])
2179
2413
 
2180
- const label = repo.detached === true ? (repo.oid ?? t('branch.detached')) : (repo.branch ?? t('branch.detached'))
2414
+ const diverged = summary.ahead + summary.behind
2415
+ const detail = [
2416
+ `${t('branch.current')}: ${summary.label}`,
2417
+ summary.detached ? t('branch.detached') : '',
2418
+ summary.ahead > 0 ? fill(t('branch.ahead'), { count: summary.ahead }) : '',
2419
+ summary.behind > 0 ? fill(t('branch.behind'), { count: summary.behind }) : '',
2420
+ ].filter((part) => part !== '').join(' · ')
2421
+
2181
2422
  return h(
2182
2423
  'div',
2183
2424
  { className: 'gp-menu-wrap', ref },
2184
2425
  h(
2185
2426
  'button',
2186
- { type: 'button', className: 'gp-branch', title: `${t('branch.current')}: ${label}`, onClick: () => setOpen((value) => !value) },
2187
- h(BranchIcon, { size: 12 }),
2188
- h('span', null, label),
2427
+ {
2428
+ type: 'button',
2429
+ className: 'gp-branch',
2430
+ title: detail,
2431
+ 'aria-haspopup': 'menu',
2432
+ 'aria-expanded': open === true,
2433
+ onClick: () => setOpen((value) => !value),
2434
+ },
2435
+ h(BranchIcon, { size: 11 }),
2436
+ h('span', { className: 'gp-branch-name' }, summary.label),
2437
+ summary.detached === true || diverged === 0
2438
+ ? null
2439
+ : h(
2440
+ 'span',
2441
+ { className: 'gp-branch-meta' },
2442
+ summary.ahead > 0 ? h('span', { className: 'up' }, `↑${summary.ahead}`) : null,
2443
+ summary.behind > 0 ? h('span', { className: 'down' }, `↓${summary.behind}`) : null,
2444
+ ),
2189
2445
  ),
2190
2446
  open
2191
2447
  ? h(
2192
2448
  'div',
2193
- { className: 'gp-menu', role: 'menu' },
2449
+ { className: `gp-menu${placement === 'up' ? ' up' : ''}`, role: 'menu' },
2450
+ h('div', { className: 'gp-menu-title' }, t('branch.local')),
2194
2451
  (branches?.local ?? []).map((branch) =>
2195
2452
  h(
2196
2453
  'button',
@@ -2206,10 +2463,10 @@ window.__ModuleLoader__.load({
2206
2463
  },
2207
2464
  h(BranchIcon, { size: 12 }),
2208
2465
  h('span', null, branch.name),
2209
- branch.current === true ? h('span', { className: 'gp-tag' }, t('branch.current')) : null,
2466
+ branch.current === true ? h('span', { className: 'gp-menu-check' }, h(CheckIcon, null)) : null,
2210
2467
  ),
2211
2468
  ),
2212
- (branches?.local ?? []).length === 0 ? h('div', { className: 'gp-note' }, t('graph.loading')) : null,
2469
+ (branches?.local ?? []).length === 0 ? h('div', { className: 'gp-note' }, t('empty.loading')) : null,
2213
2470
  )
2214
2471
  : null,
2215
2472
  )
@@ -2314,6 +2571,7 @@ window.__ModuleLoader__.load({
2314
2571
  h(
2315
2572
  'div',
2316
2573
  { className: 'gp-diff-head' },
2574
+ typeof request.path === 'string' ? h(FileTypeIcon, { path: request.path }) : null,
2317
2575
  h('span', { className: 'gp-path', title: request.path ?? request.hash ?? t('diff.title') }, request.path ?? request.hash ?? t('diff.title')),
2318
2576
  mode === undefined ? null : h('span', { className: 'gp-tag' }, mode),
2319
2577
  parsed.additions > 0 ? h('span', { className: 'gp-add' }, `+${parsed.additions}`) : null,
@@ -2477,6 +2735,8 @@ window.__ModuleLoader__.load({
2477
2735
  __test: {
2478
2736
  buildReviewPrompt,
2479
2737
  ChangeBadge,
2738
+ branchSummaryOf,
2739
+ messages: { zh, en },
2480
2740
  changeKindOf,
2481
2741
  diffRequestFor,
2482
2742
  discardRequestFor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qzsy/dsh-git-panel",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A Git section for the right Sidebar: workspace changes, a commit box, stage/discard, a diff preview tab, and a commit graph — modelled on the Git module of the local Cursor IDE.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",