@xia-sc/dsh-git 0.5.3 → 0.6.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.
package/README.en.md CHANGED
@@ -15,13 +15,16 @@ new-branch-from-base.
15
15
  ## UI
16
16
 
17
17
  - **Floating block** (`shell.overlay`): collapsed = renders nothing (no
18
- floating element that could cover the input); expanded = the full Git
19
- workbench (status line, branch switcher with **dirty-tree pre-check** —
20
- selecting a branch while uncommitted changes exist shows a warning listing
21
- the affected files instead of switching, with a "switch anyway" escape
22
- hatch, fetch/pull actions, a **commit area** stage-all button, draft-basis
23
- picker, "✨ AI draft" button, message input and commit button collapsible
24
- changes and recent-commit lists). **Operation feedback is pinned to the very
18
+ floating element that could cover the input); expanded = one workflow read
19
+ top-down: status line (pending count, ahead/behind, upstream) the branch
20
+ switcher with its **dirty-tree pre-check** (selecting a branch while
21
+ uncommitted changes exist warns with the affected files instead of switching,
22
+ plus a "switch anyway" escape hatch) a **network toolbar** (Fetch / Pull /
23
+ Push, three equal columns, Push being the primary button) a **commit card**
24
+ (Stage all + draft basis + "✨ AI draft" in equal columns, the message input
25
+ below it and the commit button) → two list cards. Every button group is split
26
+ evenly instead of sizing itself to its label.
27
+ **Operation feedback is pinned to the very
25
28
  top of the panel and stays one line tall** — the busy label and the last
26
29
  operation's result are the first rows of the body. The result is a localized
27
30
  phrase ("Pushed", "Staged everything", "Switched to x"); git's own output (a
@@ -36,6 +39,18 @@ new-branch-from-base.
36
39
  new branch name + base-branch picker (local branches or full remote refs
37
40
  like `origin/feature/x`) — confirming creates the branch from the base and
38
41
  switches to it.
42
+ - **Changes and recent commits are two cards**: a change row is "monospace path
43
+ + status chip" (path first, so the paths line up in a column; modified=amber,
44
+ added/untracked=green, deleted/conflict=red, renamed=blue; the file the diff
45
+ pane is showing is highlighted), a commit row is "short sha + subject". Each
46
+ card's header (`▾ Changes 12`) collapses the whole section and starts
47
+ **expanded** — a Git panel must not hide the user's own changes; a list past
48
+ 8 / 5 rows offers "Show all N" at the bottom, which is a separate state, so the
49
+ chevron always points the right way.
50
+ - **Refreshing never flashes**: clicking ↻ or any post-action re-read keeps the
51
+ panel's content (a same-workspace re-read does not fall back to a "loading"
52
+ line and snap back); the header's refresh button greys out and reads
53
+ "Refreshing…".
39
54
  - **Click a change to see its diff**: clicking any row of the change list grows
40
55
  the panel from its 400px single column into two panes — the full workbench on
41
56
  the left, that file's unified diff on the right (see *The diff viewer* below).
@@ -62,6 +77,10 @@ the first selection expands the list to its full length.
62
77
  panel (the workbench keeps its width) and the diff takes the rest, 1:1 with the
63
78
  pointer because the panel anchors its left edge on grab. Double-clicking the
64
79
  edge resets the width, and the panel always stays inside the viewport.
80
+ - **A stable box while the diff is open**: selecting a file pins the panel's height
81
+ (both columns run to the bottom), so the diff arriving, switching files and
82
+ collapsing again only scroll inside the panes — the panel never resizes under
83
+ the pointer.
65
84
  - **Staged / unstaged**: by default the pane follows the data — unstaged when
66
85
  that side has anything, staged otherwise — with two chips to switch by hand
67
86
  (an empty side is dimmed). When a file has changes on both sides, one click
package/README.md CHANGED
@@ -13,12 +13,13 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
13
13
  ## 界面
14
14
 
15
15
  - **悬浮面板**(`shell.overlay`):折叠时不渲染任何元素(不会遮挡输入框);
16
- 展开后是完整的 Git 工作台(状态行、带 **脏树预检** 的分支切换器——在有
17
- 未提交修改时选择分支,会先显示受影响文件列表警告而不是直接切换,并提供
18
- "仍要切换"按钮;fetch/pull 操作;**提交区**——"暂存全部"按钮、AI 生成依据
19
- 选择器、" AI 生成"按钮、提交信息输入框(多行文本框,回车换行、
20
- Ctrl/Cmd+Enter 提交)与提交按钮;可折叠的变更列表与
21
- 最近提交列表)。**操作反馈固定钉在面板最上边,而且只有一行**——"推送中…"和上次操作的结果都在
16
+ 展开后自上而下是一条工作流:状态行(未提交计数、领先/落后、上游)→
17
+ 带 **脏树预检** 的分支切换器(有未提交修改时选择分支,会先显示受影响文件列表
18
+ 警告而不是直接切换,并提供"仍要切换"按钮)→ **网络工具条**(拉取更新 / 拉取合并 /
19
+ 推送,三等宽,"推送"是主按钮)→ **提交卡片**("暂存全部" + AI 生成依据 +
20
+ "✨ AI 生成"三等宽,下面是提交信息输入框(多行文本框,回车换行、Ctrl/Cmd+Enter
21
+ 提交)与提交按钮)→ 两张列表卡片。按钮一律**等宽分栏**,不再是一排宽窄不一的按钮。
22
+ **操作反馈固定钉在面板最上边,而且只有一行**——"推送中…"和上次操作的结果都在
22
23
  body 的第一行;结果是一个中文短句("已推送"/"已暂存全部"/"已切换到 xxx"),git 自己的输出
23
24
  (push 的 sideband banner、`LF will be replaced by CRLF` 这类提示)收在右侧的"详情 ▾"里,点开才展开,
24
25
  不会被变更列表和最近提交顶到看不见的地方。面板**可通过顶栏拖动**(按住带 Git 标题的
@@ -27,6 +28,13 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
27
28
  分支切换器旁边的"+ 新建分支"按钮会展开一个内联表单:新分支名 + 基分支
28
29
  选择器(本地分支或 `origin/feature/x` 这样的完整远端引用)——确认后从
29
30
  该基分支创建新分支并切换过去。
31
+ - **变更 / 最近提交两张卡片**:变更列表每行是「等宽路径 + 状态色标签」(路径在左,
32
+ 所以一列路径对得齐;修改=橙、新增·未跟踪=绿、删除·冲突=红、重命名=蓝,
33
+ 当前正在看的那一行整行高亮);最近提交每行是「短 sha + 主题」。两张卡片的标题行
34
+ (`▾ 变更 12`)点一下整段收起/展开,**默认展开**——面板不该把用户自己的改动藏起来;
35
+ 列表超过 8 / 5 行时底部出现「显示全部 N 项」,与折叠是两件事,所以箭头永远指对方向。
36
+ - **刷新不闪**:点 ↻ 或任何操作之后的重读都**保留面板内容**(同一个工作区不会退回
37
+ 一行 "读取中…" 再弹回来),只在标题栏把刷新按钮置灰并写着"刷新中…"。
30
38
  - **点击变更看差异**:点变更列表里的任意一行,面板会从 400px 的单栏展开成
31
39
  两栏——左边照旧是完整工作台,右边是该文件的 unified diff(见下文
32
40
  「差异查看」)。再点同一行、或点差异标题栏的 `×`,就收回单栏。
@@ -47,6 +55,8 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
47
55
  - **可拖拽调宽**:拖动**面板最右侧那条边**即改差异栏宽度——左栏(工作台)宽度不变,
48
56
  面板整体变宽、差异栏跟着长;双击这条边复位。面板始终留在视口内,窗口变窄时
49
57
  会自动收回;拖动时面板会就地锚定左边缘,所以指针与这条边是 1:1 跟手的。
58
+ - **打开差异时尺寸是稳定的**:一选中文件,面板就定好了高度(两栏都铺到面板底部),
59
+ diff 读回来、切文件、来回点收起都只在栏内滚动,面板本身不会跟着内容跳一下。
50
60
  - **未暂存 / 已暂存**:默认自动跟随数据——未暂存有内容就显示未暂存,否则显示
51
61
  已暂存;顶部两个 chip 可手动切换,空的一侧置灰。同一个文件两边都有改动时,
52
62
  一次点击看到的是最关心的那一侧,另一侧一键可达。
package/lib/client.js CHANGED
@@ -38,6 +38,7 @@ window.__ModuleLoader__.load({
38
38
  "panel.title": "Git",
39
39
  "panel.collapse": "收起",
40
40
  "refresh.aria": "刷新 Git 状态",
41
+ "refresh.busy": "刷新中…",
41
42
  "branch.aria": "当前 Git 分支",
42
43
  "switch.aria": "切换分支",
43
44
  "switch.dirtyWarn": "有 {count} 个未提交文件(如 {paths}),切换分支可能被 git 拒绝,请先提交或暂存",
@@ -77,8 +78,12 @@ window.__ModuleLoader__.load({
77
78
  "generate.failed": "生成失败",
78
79
  "commit.placeholder": "提交信息(可多行,Ctrl+Enter 提交)…",
79
80
  "commit.empty": "请输入提交信息",
80
- "changes.title": "变更({count})",
81
+ "changes.title": "变更",
81
82
  "changes.none": "工作区干净",
83
+ // Section footer: the list previews its first N rows and this says how
84
+ // many more there are, instead of a bare "▾ 13".
85
+ "section.more": "显示全部 {count} 项",
86
+ "section.less": "只看前几项",
82
87
  "diff.title": "差异",
83
88
  "diff.open": "查看差异",
84
89
  "diff.close": "关闭差异",
@@ -137,6 +142,7 @@ window.__ModuleLoader__.load({
137
142
  "panel.title": "Git",
138
143
  "panel.collapse": "Collapse",
139
144
  "refresh.aria": "Refresh Git status",
145
+ "refresh.busy": "Refreshing…",
140
146
  "branch.aria": "Current git branch",
141
147
  "switch.aria": "Switch branch",
142
148
  "switch.dirtyWarn": "{count} uncommitted file(s) (e.g. {paths}) may block the switch — commit or stash first",
@@ -176,8 +182,10 @@ window.__ModuleLoader__.load({
176
182
  "generate.failed": "Draft failed",
177
183
  "commit.placeholder": "Commit message (multi-line allowed, Ctrl+Enter commits)…",
178
184
  "commit.empty": "Enter a commit message",
179
- "changes.title": "Changes ({count})",
185
+ "changes.title": "Changes",
180
186
  "changes.none": "Working tree clean",
187
+ "section.more": "Show all {count}",
188
+ "section.less": "Show fewer",
181
189
  "diff.title": "Diff",
182
190
  "diff.open": "View diff",
183
191
  "diff.close": "Close diff",
@@ -269,8 +277,8 @@ window.__ModuleLoader__.load({
269
277
  display: "flex",
270
278
  alignItems: "center",
271
279
  gap: 8,
272
- padding: "8px 12px",
273
- borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2))",
280
+ padding: "8px 10px",
281
+ borderBottom: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.22))",
274
282
  flex: "none",
275
283
  cursor: "move",
276
284
  userSelect: "none",
@@ -284,11 +292,39 @@ window.__ModuleLoader__.load({
284
292
  fontSize: 13,
285
293
  fontWeight: 600,
286
294
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
295
+ flex: "none",
296
+ whiteSpace: "nowrap"
297
+ },
298
+ // The workspace path takes the header's slack: it is the one field whose
299
+ // truncation must never move the buttons beside it.
300
+ cwdPath: {
287
301
  flex: "1",
288
302
  minWidth: 0,
289
303
  overflow: "hidden",
290
304
  textOverflow: "ellipsis",
291
- whiteSpace: "nowrap"
305
+ whiteSpace: "nowrap",
306
+ fontSize: 11,
307
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
308
+ textAlign: "right"
309
+ },
310
+ // Square ghost icon button: the panel's own chrome (refresh / collapse).
311
+ iconButton: {
312
+ flex: "none",
313
+ display: "inline-flex",
314
+ alignItems: "center",
315
+ justifyContent: "center",
316
+ width: 24,
317
+ height: 24,
318
+ padding: 0,
319
+ border: "1px solid transparent",
320
+ borderRadius: 6,
321
+ background: "transparent",
322
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
323
+ font: "inherit",
324
+ fontSize: 13,
325
+ lineHeight: "20px",
326
+ boxSizing: "border-box",
327
+ cursor: "pointer"
292
328
  },
293
329
  body: {
294
330
  flex: "1",
@@ -299,9 +335,36 @@ window.__ModuleLoader__.load({
299
335
  row: {
300
336
  display: "flex",
301
337
  alignItems: "center",
302
- gap: 8,
338
+ gap: 6,
303
339
  margin: "6px 0"
304
340
  },
341
+ // One action row inside a card: the gap is owned by the card, not the row.
342
+ rowTight: {
343
+ display: "flex",
344
+ alignItems: "center",
345
+ gap: 6,
346
+ minWidth: 0
347
+ },
348
+ // Equal-width action buttons: a row of auto-width labels was the messy
349
+ // part of the old layout — every group now shares the row evenly.
350
+ grow: {
351
+ flex: "1 1 0",
352
+ minWidth: 0,
353
+ padding: "0 6px",
354
+ textAlign: "center"
355
+ },
356
+ // Card grouping the whole commit flow (stage → draft → message → commit).
357
+ card: {
358
+ margin: "6px 0",
359
+ padding: 8,
360
+ display: "flex",
361
+ flexDirection: "column",
362
+ gap: 6,
363
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.22))",
364
+ borderRadius: 8,
365
+ background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))",
366
+ minWidth: 0
367
+ },
305
368
  warn: {
306
369
  display: "flex",
307
370
  alignItems: "center",
@@ -309,18 +372,22 @@ window.__ModuleLoader__.load({
309
372
  margin: "6px 0",
310
373
  padding: "6px 8px",
311
374
  borderRadius: 8,
312
- background: "var(--dsw-alias-state-warning-tertiary, rgba(245,158,11,0.14))",
313
- border: "1px solid var(--dsw-alias-state-warning-secondary, rgba(245,158,11,0.4))",
314
- color: "var(--dsw-alias-state-warning-primary, #f59e0b)",
375
+ background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0a020) 14%, transparent)",
376
+ border: "1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0a020) 40%, transparent)",
377
+ color: "var(--dsw-alias-state-warn-primary, #f0a020)",
315
378
  fontSize: 12,
316
379
  lineHeight: "16px"
317
380
  },
381
+ // The branch chip — only for the detached-HEAD notice, so it carries the
382
+ // warning colour rather than the accent.
318
383
  chip: {
319
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.12))",
320
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
321
- borderRadius: 999,
384
+ flex: "none",
385
+ maxWidth: "62%",
322
386
  padding: "0 8px",
323
- fontWeight: 500,
387
+ borderRadius: 6,
388
+ background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0a020) 16%, transparent)",
389
+ color: "var(--dsw-alias-state-warn-primary, #f0a020)",
390
+ fontWeight: 600,
324
391
  fontSize: 12,
325
392
  lineHeight: "20px",
326
393
  overflow: "hidden",
@@ -328,40 +395,58 @@ window.__ModuleLoader__.load({
328
395
  whiteSpace: "nowrap"
329
396
  },
330
397
  dirty: {
398
+ flex: "none",
331
399
  color: "var(--dsw-alias-state-warn-primary, #f0a020)",
332
400
  fontWeight: 600,
333
401
  fontSize: 12
334
402
  },
335
403
  meta: {
336
- color: "var(--dsw-alias-label-quaternary, #6b7280)",
337
- fontSize: 12
404
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
405
+ fontSize: 11
338
406
  },
339
407
  button: {
340
- background: "var(--dsw-alias-interactive-bg-hover-solid, rgba(127,127,127,0.14))",
408
+ background: "color-mix(in srgb, var(--dsw-alias-label-primary, #e5e7eb) 8%, transparent)",
341
409
  border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
342
- borderRadius: 8,
343
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
410
+ borderRadius: 7,
411
+ color: "var(--dsw-alias-label-primary, #e5e7eb)",
344
412
  font: "inherit",
345
413
  fontSize: 12,
346
- lineHeight: "22px",
414
+ lineHeight: "24px",
415
+ height: 26,
347
416
  padding: "0 10px",
417
+ boxSizing: "border-box",
348
418
  cursor: "pointer",
349
- flex: "none"
419
+ flex: "none",
420
+ whiteSpace: "nowrap",
421
+ overflow: "hidden",
422
+ textOverflow: "ellipsis"
350
423
  },
351
424
  buttonPrimary: {
352
- background: "var(--dsw-alias-state-business-primary, #4f8cff)",
425
+ // The harness's own primary button: the label token IS the fill, with
426
+ // `label-primary-foreground` as the text on top. It reads near-black on
427
+ // light and near-white on dark (an inverted contrast button).
428
+ // `--dsw-alias-brand-primary` is that same contrast colour, NOT an
429
+ // accent — pairing it with `#fff` text made 推送 a white block with
430
+ // invisible text in the dark theme, and 提交 an empty grey box.
431
+ background: "var(--dsw-alias-button-primary-fill, var(--dsw-alias-label-primary, #e5e7eb))",
353
432
  border: "1px solid transparent",
354
- borderRadius: 8,
355
- color: "#fff",
433
+ borderRadius: 7,
434
+ color: "var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-base, #1e1e1e))",
356
435
  font: "inherit",
357
436
  fontSize: 12,
358
- lineHeight: "22px",
437
+ fontWeight: 600,
438
+ lineHeight: "24px",
439
+ height: 26,
359
440
  padding: "0 10px",
441
+ boxSizing: "border-box",
360
442
  cursor: "pointer",
361
- flex: "none"
443
+ flex: "none",
444
+ whiteSpace: "nowrap",
445
+ overflow: "hidden",
446
+ textOverflow: "ellipsis"
362
447
  },
363
448
  buttonDisabled: {
364
- opacity: 0.5,
449
+ opacity: 0.4,
365
450
  cursor: "default"
366
451
  },
367
452
  select: {
@@ -369,14 +454,16 @@ window.__ModuleLoader__.load({
369
454
  // theme (dark list on dark pages) instead of the OS default
370
455
  // white; option/optgroup styles above reinforce it.
371
456
  colorScheme: "light dark",
372
- background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.1))",
373
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
374
- borderRadius: 8,
375
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
457
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.1))",
458
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
459
+ borderRadius: 7,
460
+ color: "var(--dsw-alias-label-primary, #e5e7eb)",
376
461
  font: "inherit",
377
462
  fontSize: 12,
378
- lineHeight: "20px",
379
- padding: "2px 8px",
463
+ lineHeight: "24px",
464
+ height: 26,
465
+ padding: "0 6px",
466
+ boxSizing: "border-box",
380
467
  maxWidth: 220,
381
468
  cursor: "pointer",
382
469
  flex: "1",
@@ -384,14 +471,16 @@ window.__ModuleLoader__.load({
384
471
  outline: "none"
385
472
  },
386
473
  input: {
387
- background: "transparent",
388
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
389
- borderRadius: 8,
474
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.1))",
475
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
476
+ borderRadius: 7,
390
477
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
391
478
  font: "inherit",
392
479
  fontSize: 12,
393
- lineHeight: "20px",
394
- padding: "2px 8px",
480
+ lineHeight: "24px",
481
+ height: 26,
482
+ padding: "0 8px",
483
+ boxSizing: "border-box",
395
484
  flex: "1",
396
485
  minWidth: 0,
397
486
  outline: "none"
@@ -400,47 +489,123 @@ window.__ModuleLoader__.load({
400
489
  // silently strips every line feed, so a drafted subject+body would lose
401
490
  // its body before the request was ever sent.
402
491
  textarea: {
403
- background: "transparent",
404
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
405
- borderRadius: 8,
492
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.1))",
493
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
494
+ borderRadius: 7,
406
495
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
407
496
  font: "inherit",
408
497
  fontSize: 12,
409
- lineHeight: "18px",
498
+ lineHeight: "17px",
410
499
  padding: "4px 8px",
500
+ minHeight: 46,
411
501
  flex: "1",
412
502
  minWidth: 0,
503
+ boxSizing: "border-box",
413
504
  outline: "none",
414
505
  resize: "vertical",
415
506
  overflowY: "auto"
416
507
  },
417
- sectionTitle: {
508
+ // ── list sections (changes / recent commits) ─────────────────────
509
+ // A section is a card: a full-width header toggle (chevron + label +
510
+ // count) and its rows. Collapsing hides the rows outright — the old
511
+ // header doubled as "show more" for a preview, so its chevron pointed
512
+ // the wrong way for a list that was already on screen.
513
+ section: {
514
+ marginTop: 8,
515
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.22))",
516
+ borderRadius: 8,
517
+ background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))",
518
+ overflow: "hidden"
519
+ },
520
+ sectionHeader: {
521
+ display: "flex",
522
+ alignItems: "center",
523
+ gap: 6,
524
+ width: "100%",
525
+ padding: "6px 8px",
526
+ background: "transparent",
527
+ border: "none",
528
+ color: "var(--dsw-alias-label-primary, #e5e7eb)",
529
+ font: "inherit",
418
530
  fontSize: 12,
419
531
  fontWeight: 600,
420
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
421
- margin: "10px 0 4px",
532
+ lineHeight: "18px",
533
+ boxSizing: "border-box",
534
+ cursor: "pointer",
535
+ textAlign: "left"
536
+ },
537
+ sectionChevron: {
538
+ flex: "none",
539
+ width: 10,
540
+ fontSize: 10,
541
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
542
+ },
543
+ sectionLabel: {
544
+ flex: "1",
545
+ minWidth: 0,
546
+ overflow: "hidden",
547
+ textOverflow: "ellipsis",
548
+ whiteSpace: "nowrap"
549
+ },
550
+ sectionCount: {
551
+ flex: "none",
552
+ padding: "0 6px",
553
+ borderRadius: 999,
554
+ background: "color-mix(in srgb, var(--dsw-alias-label-primary, #e5e7eb) 10%, transparent)",
555
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
556
+ fontSize: 11,
557
+ fontWeight: 500,
558
+ lineHeight: "16px"
559
+ },
560
+ sectionBody: {
561
+ padding: "0 4px 4px"
562
+ },
563
+ sectionEmpty: {
564
+ padding: "2px 6px 6px",
565
+ fontSize: 12,
566
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
567
+ },
568
+ sectionMore: {
569
+ display: "block",
570
+ width: "100%",
571
+ padding: "2px 0 4px",
572
+ background: "transparent",
573
+ border: "none",
574
+ color: "var(--dsw-alias-link, #4f8cff)",
575
+ font: "inherit",
576
+ fontSize: 11,
422
577
  cursor: "pointer",
423
- userSelect: "none"
578
+ textAlign: "center"
424
579
  },
425
580
  changeRow: {
426
581
  display: "flex",
427
582
  alignItems: "center",
428
583
  gap: 8,
429
584
  fontSize: 12,
430
- lineHeight: "20px",
431
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
432
- padding: "1px 6px",
433
- margin: "0 -6px",
585
+ lineHeight: "18px",
586
+ padding: "3px 6px",
434
587
  borderRadius: 6,
435
588
  minWidth: 0,
436
589
  cursor: "pointer"
437
590
  },
438
591
  changePath: {
592
+ flex: "1",
593
+ minWidth: 0,
439
594
  overflow: "hidden",
440
595
  textOverflow: "ellipsis",
441
596
  whiteSpace: "nowrap",
442
- flex: "1",
443
- minWidth: 0
597
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
598
+ color: "var(--dsw-alias-label-primary, #e5e7eb)"
599
+ },
600
+ // Per-status chip; `color-mix(currentColor)` gives every status the same
601
+ // tint shape from whichever state colour it carries.
602
+ changeTag: {
603
+ flex: "none",
604
+ padding: "0 6px",
605
+ borderRadius: 4,
606
+ fontSize: 11,
607
+ lineHeight: "16px",
608
+ background: "color-mix(in srgb, currentColor 15%, transparent)"
444
609
  },
445
610
  // A change row is the diff viewer's navigation: it has to read as a
446
611
  // control, and to show which file the right-hand pane is showing.
@@ -449,14 +614,12 @@ window.__ModuleLoader__.load({
449
614
  alignItems: "center",
450
615
  gap: 8,
451
616
  fontSize: 12,
452
- lineHeight: "20px",
453
- color: "var(--dsw-alias-label-primary, #e5e7eb)",
454
- padding: "1px 6px",
455
- margin: "0 -6px",
617
+ lineHeight: "18px",
618
+ padding: "3px 6px",
456
619
  borderRadius: 6,
457
620
  minWidth: 0,
458
621
  cursor: "pointer",
459
- background: "var(--dsw-alias-interactive-bg-hover-solid, rgba(127,127,127,0.18))"
622
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4f8cff) 15%, transparent)"
460
623
  },
461
624
  // ── two-pane (changes + diff) layout ────────────────────────────
462
625
  // `bodySplit` replaces `body` while a file is selected: the left pane
@@ -528,9 +691,9 @@ window.__ModuleLoader__.load({
528
691
  lineHeight: "18px",
529
692
  padding: "0 8px",
530
693
  borderRadius: 999,
531
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
694
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.25))",
532
695
  background: "transparent",
533
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
696
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
534
697
  font: "inherit",
535
698
  fontSize: 11,
536
699
  cursor: "pointer"
@@ -541,7 +704,7 @@ window.__ModuleLoader__.load({
541
704
  padding: "0 8px",
542
705
  borderRadius: 999,
543
706
  border: "1px solid transparent",
544
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.18))",
707
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4f8cff) 16%, transparent)",
545
708
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
546
709
  font: "inherit",
547
710
  fontSize: 11,
@@ -571,7 +734,7 @@ window.__ModuleLoader__.load({
571
734
  width: 34,
572
735
  textAlign: "right",
573
736
  paddingRight: 6,
574
- color: "var(--dsw-alias-label-quaternary, #6b7280)",
737
+ color: "var(--dsw-alias-label-tertiary, #6b7280)",
575
738
  userSelect: "none",
576
739
  WebkitUserSelect: "none"
577
740
  },
@@ -595,7 +758,7 @@ window.__ModuleLoader__.load({
595
758
  color: "var(--dsw-alias-state-business-primary, #4f8cff)"
596
759
  },
597
760
  diffMetaRow: {
598
- color: "var(--dsw-alias-label-quaternary, #6b7280)"
761
+ color: "var(--dsw-alias-label-tertiary, #6b7280)"
599
762
  },
600
763
  diffNotice: {
601
764
  margin: "6px 10px",
@@ -603,7 +766,7 @@ window.__ModuleLoader__.load({
603
766
  borderRadius: 8,
604
767
  fontSize: 12,
605
768
  lineHeight: "18px",
606
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.12))",
769
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.12))",
607
770
  color: "var(--dsw-alias-label-secondary, #c8ccd2)"
608
771
  },
609
772
  statusTag: {
@@ -612,22 +775,32 @@ window.__ModuleLoader__.load({
612
775
  lineHeight: "16px",
613
776
  padding: "0 6px",
614
777
  borderRadius: 4,
615
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.12))"
778
+ background: "color-mix(in srgb, var(--dsw-alias-label-primary, #e5e7eb) 10%, transparent)",
779
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
616
780
  },
617
781
  logRow: {
618
782
  display: "flex",
619
783
  alignItems: "center",
620
784
  gap: 8,
621
785
  fontSize: 12,
622
- lineHeight: "20px",
623
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
624
- padding: "1px 0",
786
+ lineHeight: "18px",
787
+ padding: "3px 6px",
788
+ borderRadius: 6,
625
789
  minWidth: 0
626
790
  },
627
791
  logSha: {
628
792
  flex: "none",
629
- color: "var(--dsw-alias-state-business-primary, #4f8cff)",
630
- fontFamily: "monospace"
793
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
794
+ fontSize: 11,
795
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
796
+ },
797
+ logSubject: {
798
+ flex: "1",
799
+ minWidth: 0,
800
+ overflow: "hidden",
801
+ textOverflow: "ellipsis",
802
+ whiteSpace: "nowrap",
803
+ color: "var(--dsw-alias-label-primary, #e5e7eb)"
631
804
  },
632
805
  output: {
633
806
  // Sits at the top of the body, so the breathing room belongs below it.
@@ -667,8 +840,8 @@ window.__ModuleLoader__.load({
667
840
  outputDetail: {
668
841
  marginTop: 6,
669
842
  paddingTop: 6,
670
- borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,0.35))",
671
- fontFamily: "monospace",
843
+ borderTop: "1px solid color-mix(in srgb, currentColor 30%, transparent)",
844
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
672
845
  fontSize: 11,
673
846
  lineHeight: "16px",
674
847
  wordBreak: "break-word",
@@ -677,11 +850,11 @@ window.__ModuleLoader__.load({
677
850
  whiteSpace: "pre-wrap"
678
851
  },
679
852
  outputOk: {
680
- background: "var(--dsw-alias-state-success-tertiary, rgba(52,199,89,0.12))",
853
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary, #34c759) 14%, transparent)",
681
854
  color: "var(--dsw-alias-state-success-primary, #34c759)"
682
855
  },
683
856
  outputError: {
684
- background: "var(--dsw-alias-state-error-tertiary, rgba(239,68,68,0.12))",
857
+ background: "color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 14%, transparent)",
685
858
  color: "var(--dsw-alias-state-error-primary, #ef4444)"
686
859
  },
687
860
  dockWrap: {
@@ -691,7 +864,7 @@ window.__ModuleLoader__.load({
691
864
  maxWidth: "100%",
692
865
  fontSize: 12,
693
866
  lineHeight: "20px",
694
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
867
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
695
868
  whiteSpace: "nowrap",
696
869
  overflow: "hidden",
697
870
  textOverflow: "ellipsis",
@@ -824,6 +997,84 @@ window.__ModuleLoader__.load({
824
997
  }, children);
825
998
  }
826
999
 
1000
+ /** Rows a collapsed-by-default list shows before its "show all" footer. */
1001
+ var CHANGES_PREVIEW = 8;
1002
+ /** Same, for the recent-commit list (shorter rows, less to say). */
1003
+ var LOG_PREVIEW = 5;
1004
+
1005
+ /** The state colour one porcelain status carries, in theme tokens. */
1006
+ function changeTagStyle(status) {
1007
+ var color = status === "added" || status === "untracked"
1008
+ ? "var(--dsw-alias-state-success-primary, #34c759)"
1009
+ : status === "deleted" || status === "conflict"
1010
+ ? "var(--dsw-alias-state-error-primary, #ef4444)"
1011
+ : status === "renamed" || status === "copied"
1012
+ // `state-business-primary` is the real accent (deepseek blue);
1013
+ // `brand-primary` is the inverted contrast colour and would make
1014
+ // this chip white-on-white in the dark theme.
1015
+ ? "var(--dsw-alias-state-business-primary, #4f8cff)"
1016
+ : status === "modified" || status === "changed"
1017
+ ? "var(--dsw-alias-state-warn-primary, #f0a020)"
1018
+ : "var(--dsw-alias-label-secondary, #c8ccd2)";
1019
+ // `changeTag`'s background is `color-mix(currentColor…)`, so the tint
1020
+ // follows whichever colour the status carries.
1021
+ return Object.assign({}, S.changeTag, { color: color });
1022
+ }
1023
+
1024
+ /**
1025
+ * One collapsible list section: a full-width header toggle (chevron +
1026
+ * label + count) and, while open, its rows plus a footer that expands the
1027
+ * preview. Two separate states on purpose — the header collapses the
1028
+ * section, the footer widens it — because one flag doing both made the
1029
+ * chevron point the wrong way for a list that was already on screen.
1030
+ *
1031
+ * @param spec - `{ hook, label, count, open, all, cap, empty, rows, onToggle, onMore }`.
1032
+ * @param t - the locale translator.
1033
+ * @returns the section element.
1034
+ */
1035
+ function sectionView(spec, t) {
1036
+ var children = [h("button", {
1037
+ key: "header",
1038
+ type: "button",
1039
+ "data-dsh-git": spec.hook + "-header",
1040
+ style: S.sectionHeader,
1041
+ "aria-expanded": spec.open ? "true" : "false",
1042
+ onClick: spec.onToggle
1043
+ }, [
1044
+ h("span", { key: "chevron", style: S.sectionChevron }, spec.open ? "▾" : "▸"),
1045
+ h("span", { key: "label", style: S.sectionLabel }, spec.label),
1046
+ h("span", { key: "count", style: S.sectionCount }, String(spec.count))
1047
+ ])];
1048
+ if (spec.open) {
1049
+ var previewed = spec.all || spec.count <= spec.cap;
1050
+ children.push(h("div", {
1051
+ key: "body",
1052
+ "data-dsh-git": spec.hook + "-body",
1053
+ style: S.sectionBody
1054
+ }, spec.count === 0
1055
+ ? h("div", { key: "empty", style: S.sectionEmpty }, spec.empty)
1056
+ : (previewed ? spec.rows : spec.rows.slice(0, spec.cap))));
1057
+ if (!previewed) {
1058
+ children.push(h("button", {
1059
+ key: "more",
1060
+ type: "button",
1061
+ "data-dsh-git": spec.hook + "-more",
1062
+ style: S.sectionMore,
1063
+ onClick: spec.onMore
1064
+ }, t("section.more", { count: spec.count })));
1065
+ } else if (spec.all && spec.count > spec.cap) {
1066
+ children.push(h("button", {
1067
+ key: "more",
1068
+ type: "button",
1069
+ "data-dsh-git": spec.hook + "-more",
1070
+ style: S.sectionMore,
1071
+ onClick: spec.onMore
1072
+ }, t("section.less")));
1073
+ }
1074
+ }
1075
+ return h("div", { key: spec.hook, "data-dsh-git": spec.hook, style: S.section }, children);
1076
+ }
1077
+
827
1078
  /** Call one /dsh-git-rpc endpoint. */
828
1079
  function rpc(ctx, endpoint, args) {
829
1080
  return ctx.connection.rpc.call("/dsh-git-rpc", endpoint, { args: args || {} }).then(sanitizeResult);
@@ -881,6 +1132,7 @@ window.__ModuleLoader__.load({
881
1132
  commits: [],
882
1133
  error: null,
883
1134
  busy: null,
1135
+ refreshing: false,
884
1136
  lastResult: null,
885
1137
  panelOpen: false
886
1138
  };
@@ -942,7 +1194,19 @@ window.__ModuleLoader__.load({
942
1194
 
943
1195
  function doRefresh(cwd) {
944
1196
  var mySeq = ++seq;
945
- emit(Object.assign({}, state, { sessionId: sessionId, cwd: cwd, phase: cwd ? "loading" : "idle", error: null }));
1197
+ // Re-reading the SAME workspace keeps what is already on screen.
1198
+ // Flipping the body to "loading" for a few hundred milliseconds
1199
+ // collapsed the whole workbench and snapped it back — that is the
1200
+ // jump you saw after clicking ↻ or any action button. Only a first
1201
+ // load (or a workspace switch) has nothing to keep.
1202
+ var keep = cwd !== null && cwd === state.cwd && state.phase === "ready";
1203
+ emit(Object.assign({}, state, {
1204
+ sessionId: sessionId,
1205
+ cwd: cwd,
1206
+ phase: keep ? state.phase : (cwd ? "loading" : "idle"),
1207
+ refreshing: keep,
1208
+ error: keep ? state.error : null
1209
+ }));
946
1210
  if (!cwd) return Promise.resolve();
947
1211
  return Promise.all([
948
1212
  verbs.status(cwd),
@@ -1052,7 +1316,7 @@ window.__ModuleLoader__.load({
1052
1316
  };
1053
1317
  var OPTGROUP_STYLE = {
1054
1318
  background: "var(--dsw-alias-bg-base, #1e1e1e)",
1055
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
1319
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
1056
1320
  fontWeight: 600
1057
1321
  };
1058
1322
 
@@ -1203,6 +1467,32 @@ window.__ModuleLoader__.load({
1203
1467
  var PANEL_MARGIN = 24;
1204
1468
  /** Grab width of the panel's right-edge resize handle. */
1205
1469
  var PANEL_RESIZE_GRIP = 6;
1470
+ /**
1471
+ * Height the panel takes while the diff pane is open: a fixed height, not a
1472
+ * cap. With a cap the panel's height followed its content, so the moment the
1473
+ * diff arrived the pane grew and — anchored by `bottom` — the whole panel
1474
+ * jumped (the flash you saw when opening a diff). The `min()` keeps the panel
1475
+ * on screen in a short window (168px is the panel's bottom offset).
1476
+ */
1477
+ var DIFF_PANEL_HEIGHT = "min(72vh, calc(100vh - 184px))";
1478
+
1479
+ /**
1480
+ * The panel's box in either mode: the compact single column, or workbench +
1481
+ * diff with a height that cannot move once the pane is open.
1482
+ * @param selected - the change the diff pane shows, or null.
1483
+ * @param diffWidth - width of the diff pane.
1484
+ * @returns the panel's inline style.
1485
+ */
1486
+ function panelBoxStyle(selected, diffWidth) {
1487
+ var box = Object.assign({}, S.panel, {
1488
+ width: selected === null ? S.panel.width : DIFF_LEFT_PANE_WIDTH + PANEL_BORDER_WIDTH + diffWidth
1489
+ });
1490
+ if (selected !== null) {
1491
+ box.height = DIFF_PANEL_HEIGHT;
1492
+ box.maxHeight = DIFF_PANEL_HEIGHT;
1493
+ }
1494
+ return box;
1495
+ }
1206
1496
 
1207
1497
  /** The last path segment, for chip-sized text. */
1208
1498
  function baseName(file) {
@@ -1553,12 +1843,22 @@ window.__ModuleLoader__.load({
1553
1843
  var generateModeRef = React.useState("staged");
1554
1844
  var generateMode = generateModeRef[0];
1555
1845
  var setGenerateMode = generateModeRef[1];
1556
- var changesOpenRef = React.useState(false);
1846
+ // Sections start OPEN: a Git panel that hides the user's own changes
1847
+ // behind a chevron is a panel that looks empty. Collapsing is the
1848
+ // explicit choice, and widening a long list past its preview is a
1849
+ // separate one (`…All`), so the chevron always tells the truth.
1850
+ var changesOpenRef = React.useState(true);
1557
1851
  var changesOpen = changesOpenRef[0];
1558
1852
  var setChangesOpen = changesOpenRef[1];
1559
- var logOpenRef = React.useState(false);
1853
+ var changesAllRef = React.useState(false);
1854
+ var changesAll = changesAllRef[0];
1855
+ var setChangesAll = changesAllRef[1];
1856
+ var logOpenRef = React.useState(true);
1560
1857
  var logOpen = logOpenRef[0];
1561
1858
  var setLogOpen = logOpenRef[1];
1859
+ var logAllRef = React.useState(false);
1860
+ var logAll = logAllRef[0];
1861
+ var setLogAll = logAllRef[1];
1562
1862
  // Whether the last operation's git output is expanded. A new result
1563
1863
  // always starts collapsed: the summary line is the notification, the
1564
1864
  // raw output is opt-in.
@@ -1820,11 +2120,7 @@ window.__ModuleLoader__.load({
1820
2120
  // while nothing is selected, and the two-pane width (workbench + diff)
1821
2121
  // once a file is open. Dragging the panel's right edge is what resizes
1822
2122
  // the diff, so the panel grows and shrinks with it.
1823
- var panelWidth = selected === null ? S.panel.width : DIFF_LEFT_PANE_WIDTH + PANEL_BORDER_WIDTH + diffWidth;
1824
- var panelStyle = Object.assign({}, S.panel, {
1825
- width: panelWidth,
1826
- maxHeight: selected === null ? S.panel.maxHeight : "72vh"
1827
- });
2123
+ var panelStyle = panelBoxStyle(selected, diffWidth);
1828
2124
  if (dragPos) {
1829
2125
  panelStyle = Object.assign(panelStyle, {
1830
2126
  left: dragPos.left,
@@ -1858,20 +2154,24 @@ window.__ModuleLoader__.load({
1858
2154
  "data-dsh-git": "panel-drag"
1859
2155
  }, [
1860
2156
  h("span", { key: "title", style: S.title }, "⎇ " + t("panel.title")),
1861
- h("span", { key: "cwd", style: S.meta, title: cwd }, shortText(cwd, 60)),
2157
+ h("span", { key: "cwd", style: S.cwdPath, title: cwd }, shortText(cwd, 80)),
1862
2158
  h("button", {
1863
2159
  key: "refresh",
1864
2160
  type: "button",
1865
- style: S.button,
1866
- disabled: busy !== null,
2161
+ // A re-read keeps the whole workbench on screen and reports itself
2162
+ // here instead of collapsing the body to a "loading" line and
2163
+ // snapping back — that flash was the jump.
2164
+ style: Object.assign({}, S.iconButton, busy !== null || state.refreshing ? S.buttonDisabled : null),
2165
+ disabled: busy !== null || state.refreshing === true,
1867
2166
  onClick: function () { store.refresh(state.cwd); },
1868
2167
  "aria-label": t("refresh.aria"),
1869
- title: t("refresh.aria")
1870
- }, ""),
2168
+ "aria-busy": state.refreshing === true ? "true" : "false",
2169
+ title: state.refreshing === true ? t("refresh.busy") : t("refresh.aria")
2170
+ }, state.refreshing === true ? "⋯" : "↻"),
1871
2171
  h("button", {
1872
2172
  key: "collapse",
1873
2173
  type: "button",
1874
- style: S.button,
2174
+ style: S.iconButton,
1875
2175
  onClick: function () { store.setPanelOpen(false); },
1876
2176
  "aria-label": t("panel.collapse"),
1877
2177
  title: t("panel.collapse")
@@ -1896,15 +2196,21 @@ window.__ModuleLoader__.load({
1896
2196
  } else if (!state.repo) {
1897
2197
  bodyChildren.push(h("div", { key: "norepo", style: S.meta }, t("notRepo")));
1898
2198
  } else {
1899
- // status line
2199
+ // status line — the branch itself lives in the select right below,
2200
+ // so this row carries only what the select cannot say: pending
2201
+ // changes and how the branch sits against its upstream.
1900
2202
  var statusLine = [];
1901
2203
  var branchText = state.branch;
1902
2204
  if (!branchText && state.detached) {
1903
2205
  branchText = t("detached") + (state.oid ? " " + state.oid.slice(0, 7) : "");
1904
2206
  }
1905
- statusLine.push(h("span", { key: "chip", style: S.chip, title: branchText || cwd }, branchText || "—"));
2207
+ if (state.detached) {
2208
+ statusLine.push(h("span", { key: "chip", style: S.chip, title: branchText }, branchText));
2209
+ }
1906
2210
  if (state.dirty > 0) {
1907
- statusLine.push(h("span", { key: "dirty", style: S.dirty, title: t("dirty", { count: state.dirty }) }, "● " + state.dirty));
2211
+ statusLine.push(h("span", { key: "dirty", style: S.dirty, title: t("dirty", { count: state.dirty }) }, "● " + t("dirty", { count: state.dirty })));
2212
+ } else {
2213
+ statusLine.push(h("span", { key: "clean", style: S.meta }, "✓ " + t("changes.none")));
1908
2214
  }
1909
2215
  if (state.ahead > 0 || state.behind > 0) {
1910
2216
  statusLine.push(h("span", { key: "ab", style: S.meta }, "↑" + state.ahead + " ↓" + state.behind));
@@ -2024,94 +2330,96 @@ window.__ModuleLoader__.load({
2024
2330
  ]));
2025
2331
  }
2026
2332
 
2027
- // network + commit row
2333
+ // network actions three equal columns. Auto-width labels used to
2334
+ // give each button a different width for the same job, which is what
2335
+ // made this row read as a pile rather than a toolbar.
2028
2336
  bodyChildren.push(h("div", { key: "net", style: S.row }, [
2029
2337
  h("button", {
2030
2338
  key: "fetch",
2031
2339
  type: "button",
2032
- style: Object.assign({}, S.button, busy !== null ? S.buttonDisabled : null),
2340
+ style: Object.assign({}, S.button, S.grow, busy !== null ? S.buttonDisabled : null),
2033
2341
  disabled: busy !== null,
2034
2342
  onClick: function () { store.act("fetch", function () { return store.verbs.fetch(state.cwd); }); }
2035
2343
  }, busy === "fetch" ? t("busy.fetch") : t("action.fetch")),
2036
2344
  h("button", {
2037
2345
  key: "pull",
2038
2346
  type: "button",
2039
- style: Object.assign({}, S.button, busy !== null ? S.buttonDisabled : null),
2347
+ style: Object.assign({}, S.button, S.grow, busy !== null ? S.buttonDisabled : null),
2040
2348
  disabled: busy !== null,
2041
2349
  onClick: function () { store.act("pull", function () { return store.verbs.pull(state.cwd); }); }
2042
2350
  }, busy === "pull" ? t("busy.pull") : t("action.pull")),
2043
2351
  h("button", {
2044
2352
  key: "push",
2045
2353
  type: "button",
2046
- style: Object.assign({}, S.buttonPrimary, busy !== null ? S.buttonDisabled : null),
2354
+ style: Object.assign({}, S.buttonPrimary, S.grow, busy !== null ? S.buttonDisabled : null),
2047
2355
  disabled: busy !== null,
2048
2356
  onClick: function () { store.act("push", function () { return store.verbs.push(state.cwd); }); }
2049
2357
  }, busy === "push" ? t("busy.push") : t("action.push"))
2050
2358
  ]));
2051
2359
 
2052
- // commit tooling: stage everything, pick the draft basis, draft.
2053
- // Ordering mirrors the real workflow (stage draft commit) and
2054
- // keeps the 400px panel readable by splitting the tool row from the
2055
- // message row.
2360
+ // commit flow, one card: stage all draft basis + draft → message →
2361
+ // commit. Grouping it keeps the 400px column readable and puts the
2362
+ // primary action (提交) in the same surface as the text it sends.
2056
2363
  var noChanges = state.dirty === 0;
2057
- bodyChildren.push(h("div", { key: "commitTools", style: S.row }, [
2058
- h("button", {
2059
- key: "stage",
2060
- type: "button",
2061
- style: Object.assign({}, S.button, busy !== null || noChanges ? S.buttonDisabled : null),
2062
- disabled: busy !== null || noChanges,
2063
- title: noChanges ? t("changes.none") : t("action.stage"),
2064
- onClick: function () { store.act("stage", function () { return store.verbs.stage(state.cwd); }); }
2065
- }, busy === "stage" ? t("busy.stage") : t("action.stage")),
2066
- h("select", {
2067
- key: "mode",
2068
- style: Object.assign({}, S.select, { flex: "none", width: "auto", maxWidth: 96 }),
2069
- value: generateMode,
2070
- disabled: busy !== null,
2071
- title: t("generate.mode.title"),
2072
- "aria-label": t("generate.mode.title"),
2073
- onChange: function (event) { setGenerateMode(event.target.value); }
2074
- }, [
2075
- h("option", { key: "staged", value: "staged", style: OPTION_STYLE }, t("generate.mode.staged")),
2076
- h("option", { key: "unstaged", value: "unstaged", style: OPTION_STYLE }, t("generate.mode.unstaged")),
2077
- h("option", { key: "all", value: "all", style: OPTION_STYLE }, t("generate.mode.all"))
2364
+ bodyChildren.push(h("div", { key: "commitCard", style: S.card }, [
2365
+ h("div", { key: "tools", style: S.rowTight }, [
2366
+ h("button", {
2367
+ key: "stage",
2368
+ type: "button",
2369
+ style: Object.assign({}, S.button, S.grow, busy !== null || noChanges ? S.buttonDisabled : null),
2370
+ disabled: busy !== null || noChanges,
2371
+ title: noChanges ? t("changes.none") : t("action.stage"),
2372
+ onClick: function () { store.act("stage", function () { return store.verbs.stage(state.cwd); }); }
2373
+ }, busy === "stage" ? t("busy.stage") : t("action.stage")),
2374
+ h("select", {
2375
+ key: "mode",
2376
+ style: Object.assign({}, S.select, S.grow, { maxWidth: "none" }),
2377
+ value: generateMode,
2378
+ disabled: busy !== null,
2379
+ title: t("generate.mode.title"),
2380
+ "aria-label": t("generate.mode.title"),
2381
+ onChange: function (event) { setGenerateMode(event.target.value); }
2382
+ }, [
2383
+ h("option", { key: "staged", value: "staged", style: OPTION_STYLE }, t("generate.mode.staged")),
2384
+ h("option", { key: "unstaged", value: "unstaged", style: OPTION_STYLE }, t("generate.mode.unstaged")),
2385
+ h("option", { key: "all", value: "all", style: OPTION_STYLE }, t("generate.mode.all"))
2386
+ ]),
2387
+ h("button", {
2388
+ key: "generate",
2389
+ type: "button",
2390
+ style: Object.assign({}, S.button, S.grow, busy !== null || noChanges ? S.buttonDisabled : null),
2391
+ disabled: busy !== null || noChanges,
2392
+ title: noChanges ? t("changes.none") : t("action.generate"),
2393
+ onClick: doGenerate
2394
+ }, busy === "generate" ? t("busy.generate") : "✨ " + t("action.generate"))
2078
2395
  ]),
2079
- h("button", {
2080
- key: "generate",
2081
- type: "button",
2082
- style: Object.assign({}, S.button, busy !== null || noChanges ? S.buttonDisabled : null),
2083
- disabled: busy !== null || noChanges,
2084
- title: noChanges ? t("changes.none") : t("action.generate"),
2085
- onClick: doGenerate
2086
- }, busy === "generate" ? t("busy.generate") : "✨ " + t("action.generate"))
2087
- ]));
2088
-
2089
- bodyChildren.push(h("div", { key: "commit", style: S.row }, [
2090
- h("textarea", {
2091
- key: "msg",
2092
- rows: 2,
2093
- style: S.textarea,
2094
- value: message,
2095
- placeholder: t("commit.placeholder"),
2096
- disabled: busy !== null,
2097
- onChange: function (event) { setMessage(event.target.value); },
2098
- // Enter inserts a newline (a message may have a body); the
2099
- // explicit commit chord is Ctrl/Cmd+Enter.
2100
- onKeyDown: function (event) {
2101
- if (event.key === "Enter" && (event.ctrlKey || event.metaKey) && message.trim() !== "" && busy === null) {
2102
- event.preventDefault();
2103
- doCommit();
2396
+ h("div", { key: "message", style: S.rowTight }, [
2397
+ h("textarea", {
2398
+ key: "msg",
2399
+ rows: 2,
2400
+ style: S.textarea,
2401
+ value: message,
2402
+ placeholder: t("commit.placeholder"),
2403
+ disabled: busy !== null,
2404
+ onChange: function (event) { setMessage(event.target.value); },
2405
+ // Enter inserts a newline (a message may have a body); the
2406
+ // explicit commit chord is Ctrl/Cmd+Enter.
2407
+ onKeyDown: function (event) {
2408
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey) && message.trim() !== "" && busy === null) {
2409
+ event.preventDefault();
2410
+ doCommit();
2411
+ }
2104
2412
  }
2105
- }
2106
- }),
2107
- h("button", {
2108
- key: "commit",
2109
- type: "button",
2110
- style: Object.assign({}, S.buttonPrimary, busy !== null || message.trim() === "" ? S.buttonDisabled : null),
2111
- disabled: busy !== null || message.trim() === "",
2112
- title: message.trim() === "" ? t("commit.empty") : undefined,
2113
- onClick: doCommit
2114
- }, busy === "commit" ? t("busy.commit") : t("action.commit"))
2413
+ }),
2414
+ h("button", {
2415
+ key: "commit",
2416
+ type: "button",
2417
+ style: Object.assign({}, S.buttonPrimary, { alignSelf: "flex-end", padding: "0 14px" }, busy !== null || message.trim() === "" ? S.buttonDisabled : null),
2418
+ disabled: busy !== null || message.trim() === "",
2419
+ title: message.trim() === "" ? t("commit.empty") : undefined,
2420
+ onClick: doCommit
2421
+ }, busy === "commit" ? t("busy.commit") : t("action.commit"))
2422
+ ])
2115
2423
  ]));
2116
2424
 
2117
2425
  function doCommit() {
@@ -2163,79 +2471,67 @@ window.__ModuleLoader__.load({
2163
2471
  });
2164
2472
  }
2165
2473
 
2166
- // changes (one row per file) — collapsible, and the diff viewer's
2167
- // navigation: clicking a row opens that file's diff on the right.
2168
- var changesRow;
2169
- if (state.changes.length === 0) {
2170
- changesRow = h("div", { key: "none", style: S.meta }, t("changes.none"));
2171
- } else {
2172
- var shown = changesOpen ? state.changes : state.changes.slice(0, 8);
2173
- changesRow = h("div", { key: "list" }, shown.map(function (change, index) {
2174
- var file = change.file !== undefined ? change.file : change.path;
2175
- var origFile = change.origFile !== undefined ? change.origFile : null;
2176
- var isActive = selected !== null && selected.file === file && selected.origFile === origFile;
2177
- return h("div", {
2178
- key: index,
2179
- style: isActive ? S.changeRowActive : S.changeRow,
2180
- role: "button",
2181
- tabIndex: 0,
2182
- title: t("diff.open"),
2183
- "aria-label": t("diff.open") + " " + change.path,
2184
- "data-dsh-git": "change-row",
2185
- "data-active": isActive === true ? "true" : "false",
2186
- onClick: function () { selectChange(change); },
2187
- onKeyDown: function (event) {
2188
- if (event.key === "Enter" || event.key === " ") {
2189
- if (event.preventDefault) event.preventDefault();
2190
- selectChange(change);
2191
- }
2474
+ // changes (one row per file) — the diff viewer's navigation:
2475
+ // clicking a row opens that file's diff on the right. The chip
2476
+ // trails the path so paths line up in one column and the eye can
2477
+ // scan them; the status only has to say which kind of change it is.
2478
+ var changesRows = state.changes.map(function (change, index) {
2479
+ var file = change.file !== undefined ? change.file : change.path;
2480
+ var origFile = change.origFile !== undefined ? change.origFile : null;
2481
+ var isActive = selected !== null && selected.file === file && selected.origFile === origFile;
2482
+ return h("div", {
2483
+ key: index,
2484
+ style: isActive ? S.changeRowActive : S.changeRow,
2485
+ role: "button",
2486
+ tabIndex: 0,
2487
+ title: change.path,
2488
+ "aria-label": t("diff.open") + " " + change.path,
2489
+ "data-dsh-git": "change-row",
2490
+ "data-active": isActive === true ? "true" : "false",
2491
+ onClick: function () { selectChange(change); },
2492
+ onKeyDown: function (event) {
2493
+ if (event.key === "Enter" || event.key === " ") {
2494
+ if (event.preventDefault) event.preventDefault();
2495
+ selectChange(change);
2192
2496
  }
2193
- }, [
2194
- h("span", { key: "tag", style: S.statusTag }, t("status." + change.status)),
2195
- h("span", { key: "path", style: S.changePath, title: change.path }, change.path)
2196
- ]);
2197
- }));
2198
- if (state.changes.length > shown.length) {
2199
- changesRow = h("div", { key: "list" }, [
2200
- changesRow,
2201
- h("button", {
2202
- key: "more",
2203
- type: "button",
2204
- style: S.button,
2205
- onClick: function () { setChangesOpen(!changesOpen); }
2206
- }, changesOpen ? "▴" : " " + (state.changes.length - shown.length))
2207
- ]);
2208
- }
2209
- }
2210
- bodyChildren.push(h("div", { key: "changes", style: S.sectionTitle, onClick: function () { setChangesOpen(!changesOpen); } }, (changesOpen ? "▾ " : "▸ ") + t("changes.title", { count: state.changes.length })));
2211
- bodyChildren.push(h("div", { key: "changesBody" }, changesRow));
2212
-
2213
- // recent commits collapsible
2214
- var logRows;
2215
- if (state.commits.length === 0) {
2216
- logRows = h("div", { key: "none", style: S.meta }, t("log.none"));
2217
- } else {
2218
- var logShown = logOpen ? state.commits : state.commits.slice(0, 5);
2219
- logRows = h("div", { key: "list" }, logShown.map(function (commit, index) {
2220
- return h("div", { key: index, style: S.logRow, title: commit.subject }, [
2221
- h("span", { key: "sha", style: S.logSha }, commit.sha),
2222
- h("span", { key: "subj", style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: "1", minWidth: 0 } }, commit.subject)
2223
- ]);
2224
- }));
2225
- if (state.commits.length > logShown.length) {
2226
- logRows = h("div", { key: "list" }, [
2227
- logRows,
2228
- h("button", {
2229
- key: "more",
2230
- type: "button",
2231
- style: S.button,
2232
- onClick: function () { setLogOpen(!logOpen); }
2233
- }, logOpen ? "▴" : "▾ " + (state.commits.length - logShown.length))
2234
- ]);
2235
- }
2236
- }
2237
- bodyChildren.push(h("div", { key: "logTitle", style: S.sectionTitle, onClick: function () { setLogOpen(!logOpen); } }, (logOpen ? "▾ " : "▸ ") + t("log.title")));
2238
- bodyChildren.push(h("div", { key: "logBody" }, logRows));
2497
+ }
2498
+ }, [
2499
+ h("span", { key: "path", style: S.changePath }, change.path),
2500
+ h("span", { key: "tag", style: changeTagStyle(change.status) }, t("status." + change.status))
2501
+ ]);
2502
+ });
2503
+ bodyChildren.push(sectionView({
2504
+ hook: "changes",
2505
+ label: t("changes.title"),
2506
+ count: state.changes.length,
2507
+ open: changesOpen,
2508
+ all: changesAll,
2509
+ cap: CHANGES_PREVIEW,
2510
+ empty: " " + t("changes.none"),
2511
+ rows: changesRows,
2512
+ onToggle: function () { setChangesOpen(!changesOpen); },
2513
+ onMore: function () { setChangesAll(!changesAll); }
2514
+ }, t));
2515
+
2516
+ // recent commits — same card, one row per commit
2517
+ var logRows = state.commits.map(function (commit, index) {
2518
+ return h("div", { key: index, style: S.logRow, title: commit.subject }, [
2519
+ h("span", { key: "sha", style: S.logSha }, commit.sha),
2520
+ h("span", { key: "subj", style: S.logSubject }, commit.subject)
2521
+ ]);
2522
+ });
2523
+ bodyChildren.push(sectionView({
2524
+ hook: "log",
2525
+ label: t("log.title"),
2526
+ count: state.commits.length,
2527
+ open: logOpen,
2528
+ all: logAll,
2529
+ cap: LOG_PREVIEW,
2530
+ empty: t("log.none"),
2531
+ rows: logRows,
2532
+ onToggle: function () { setLogOpen(!logOpen); },
2533
+ onMore: function () { setLogAll(!logAll); }
2534
+ }, t));
2239
2535
  }
2240
2536
 
2241
2537
  // Everything the open diff depends on, as one comparable string: a
@@ -2403,7 +2699,7 @@ window.__ModuleLoader__.load({
2403
2699
  maxWidth: 220,
2404
2700
  padding: "1px 10px",
2405
2701
  borderRadius: 999,
2406
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
2702
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.25))",
2407
2703
  background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.12))",
2408
2704
  color: "var(--dsw-alias-label-secondary, #c8ccd2)",
2409
2705
  font: "inherit",
@@ -2472,7 +2768,11 @@ window.__ModuleLoader__.load({
2472
2768
  GitDiffPane: GitDiffPane,
2473
2769
  DIFF_ROW: DIFF_ROW,
2474
2770
  stripAnsiEscapes: stripAnsiEscapes,
2475
- outputView: outputView
2771
+ outputView: outputView,
2772
+ sectionView: sectionView,
2773
+ changeTagStyle: changeTagStyle,
2774
+ panelBoxStyle: panelBoxStyle,
2775
+ DIFF_PANEL_HEIGHT: DIFF_PANEL_HEIGHT
2476
2776
  };
2477
2777
  return module.exports;
2478
2778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xia-sc/dsh-git",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Complete Git management for the DeepSeek Harness Web GUI: branch switch, new-branch-from-base, fetch, pull, stage-all, commit (with AI-drafted message), push, status, and a click-to-open diff viewer (staged/unstaged, resizable) — as a collapsible floating panel that follows the current session's workspace.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",