@xia-sc/dsh-git 0.5.2 → 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/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 拒绝,请先提交或暂存",
@@ -72,12 +73,17 @@ window.__ModuleLoader__.load({
72
73
  "generate.noProvider": "尚未配置模型,请先在 设置 > 模型 中添加",
73
74
  "generate.noModel": "该提供方没有可用模型",
74
75
  "generate.empty": "模型没有返回提交信息",
76
+ "generate.truncated": "模型还没写出提交信息就用完了 token 上限(思考模型会先花掉这份预算),再点一次通常即可",
75
77
  "generate.cancelled": "生成已取消",
76
78
  "generate.failed": "生成失败",
77
79
  "commit.placeholder": "提交信息(可多行,Ctrl+Enter 提交)…",
78
80
  "commit.empty": "请输入提交信息",
79
- "changes.title": "变更({count})",
81
+ "changes.title": "变更",
80
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": "只看前几项",
81
87
  "diff.title": "差异",
82
88
  "diff.open": "查看差异",
83
89
  "diff.close": "关闭差异",
@@ -109,6 +115,17 @@ window.__ModuleLoader__.load({
109
115
  "busy.checkout": "切换中…",
110
116
  "busy.createBranch": "创建分支中…",
111
117
  "output.ok": "操作成功",
118
+ "output.failed": "操作失败",
119
+ "output.detail": "详情",
120
+ "output.detailHide": "收起",
121
+ "output.fetch": "已拉取更新",
122
+ "output.pull": "已拉取合并",
123
+ "output.push": "已推送",
124
+ "output.stage": "已暂存全部",
125
+ "output.commit": "已提交",
126
+ "output.checkout": "已切换到 {branch}",
127
+ "output.createBranch": "已创建并切换到 {branch}",
128
+ "output.generate": "已生成提交信息",
112
129
  "dock.aria": "打开 Git 面板",
113
130
  "status.modified": "修改",
114
131
  "status.added": "新增",
@@ -125,6 +142,7 @@ window.__ModuleLoader__.load({
125
142
  "panel.title": "Git",
126
143
  "panel.collapse": "Collapse",
127
144
  "refresh.aria": "Refresh Git status",
145
+ "refresh.busy": "Refreshing…",
128
146
  "branch.aria": "Current git branch",
129
147
  "switch.aria": "Switch branch",
130
148
  "switch.dirtyWarn": "{count} uncommitted file(s) (e.g. {paths}) may block the switch — commit or stash first",
@@ -159,12 +177,15 @@ window.__ModuleLoader__.load({
159
177
  "generate.noProvider": "No model is configured — add one in Settings > Models",
160
178
  "generate.noModel": "That provider advertises no model",
161
179
  "generate.empty": "The model returned no commit message",
180
+ "generate.truncated": "The model hit its token cap before writing anything (a reasoning model spends that budget thinking first) — trying again usually works",
162
181
  "generate.cancelled": "Draft cancelled",
163
182
  "generate.failed": "Draft failed",
164
183
  "commit.placeholder": "Commit message (multi-line allowed, Ctrl+Enter commits)…",
165
184
  "commit.empty": "Enter a commit message",
166
- "changes.title": "Changes ({count})",
185
+ "changes.title": "Changes",
167
186
  "changes.none": "Working tree clean",
187
+ "section.more": "Show all {count}",
188
+ "section.less": "Show fewer",
168
189
  "diff.title": "Diff",
169
190
  "diff.open": "View diff",
170
191
  "diff.close": "Close diff",
@@ -196,6 +217,17 @@ window.__ModuleLoader__.load({
196
217
  "busy.checkout": "Switching…",
197
218
  "busy.createBranch": "Creating branch…",
198
219
  "output.ok": "Operation succeeded",
220
+ "output.failed": "Operation failed",
221
+ "output.detail": "Details",
222
+ "output.detailHide": "Hide",
223
+ "output.fetch": "Fetched",
224
+ "output.pull": "Pulled",
225
+ "output.push": "Pushed",
226
+ "output.stage": "Staged everything",
227
+ "output.commit": "Committed",
228
+ "output.checkout": "Switched to {branch}",
229
+ "output.createBranch": "Created and switched to {branch}",
230
+ "output.generate": "Draft ready",
199
231
  "dock.aria": "Open Git panel",
200
232
  "status.modified": "modified",
201
233
  "status.added": "added",
@@ -245,8 +277,8 @@ window.__ModuleLoader__.load({
245
277
  display: "flex",
246
278
  alignItems: "center",
247
279
  gap: 8,
248
- padding: "8px 12px",
249
- 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))",
250
282
  flex: "none",
251
283
  cursor: "move",
252
284
  userSelect: "none",
@@ -260,11 +292,39 @@ window.__ModuleLoader__.load({
260
292
  fontSize: 13,
261
293
  fontWeight: 600,
262
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: {
263
301
  flex: "1",
264
302
  minWidth: 0,
265
303
  overflow: "hidden",
266
304
  textOverflow: "ellipsis",
267
- 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"
268
328
  },
269
329
  body: {
270
330
  flex: "1",
@@ -275,9 +335,36 @@ window.__ModuleLoader__.load({
275
335
  row: {
276
336
  display: "flex",
277
337
  alignItems: "center",
278
- gap: 8,
338
+ gap: 6,
279
339
  margin: "6px 0"
280
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
+ },
281
368
  warn: {
282
369
  display: "flex",
283
370
  alignItems: "center",
@@ -285,18 +372,22 @@ window.__ModuleLoader__.load({
285
372
  margin: "6px 0",
286
373
  padding: "6px 8px",
287
374
  borderRadius: 8,
288
- background: "var(--dsw-alias-state-warning-tertiary, rgba(245,158,11,0.14))",
289
- border: "1px solid var(--dsw-alias-state-warning-secondary, rgba(245,158,11,0.4))",
290
- 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)",
291
378
  fontSize: 12,
292
379
  lineHeight: "16px"
293
380
  },
381
+ // The branch chip — only for the detached-HEAD notice, so it carries the
382
+ // warning colour rather than the accent.
294
383
  chip: {
295
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.12))",
296
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
297
- borderRadius: 999,
384
+ flex: "none",
385
+ maxWidth: "62%",
298
386
  padding: "0 8px",
299
- 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,
300
391
  fontSize: 12,
301
392
  lineHeight: "20px",
302
393
  overflow: "hidden",
@@ -304,40 +395,58 @@ window.__ModuleLoader__.load({
304
395
  whiteSpace: "nowrap"
305
396
  },
306
397
  dirty: {
398
+ flex: "none",
307
399
  color: "var(--dsw-alias-state-warn-primary, #f0a020)",
308
400
  fontWeight: 600,
309
401
  fontSize: 12
310
402
  },
311
403
  meta: {
312
- color: "var(--dsw-alias-label-quaternary, #6b7280)",
313
- fontSize: 12
404
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
405
+ fontSize: 11
314
406
  },
315
407
  button: {
316
- 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)",
317
409
  border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
318
- borderRadius: 8,
319
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
410
+ borderRadius: 7,
411
+ color: "var(--dsw-alias-label-primary, #e5e7eb)",
320
412
  font: "inherit",
321
413
  fontSize: 12,
322
- lineHeight: "22px",
414
+ lineHeight: "24px",
415
+ height: 26,
323
416
  padding: "0 10px",
417
+ boxSizing: "border-box",
324
418
  cursor: "pointer",
325
- flex: "none"
419
+ flex: "none",
420
+ whiteSpace: "nowrap",
421
+ overflow: "hidden",
422
+ textOverflow: "ellipsis"
326
423
  },
327
424
  buttonPrimary: {
328
- 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))",
329
432
  border: "1px solid transparent",
330
- borderRadius: 8,
331
- color: "#fff",
433
+ borderRadius: 7,
434
+ color: "var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-base, #1e1e1e))",
332
435
  font: "inherit",
333
436
  fontSize: 12,
334
- lineHeight: "22px",
437
+ fontWeight: 600,
438
+ lineHeight: "24px",
439
+ height: 26,
335
440
  padding: "0 10px",
441
+ boxSizing: "border-box",
336
442
  cursor: "pointer",
337
- flex: "none"
443
+ flex: "none",
444
+ whiteSpace: "nowrap",
445
+ overflow: "hidden",
446
+ textOverflow: "ellipsis"
338
447
  },
339
448
  buttonDisabled: {
340
- opacity: 0.5,
449
+ opacity: 0.4,
341
450
  cursor: "default"
342
451
  },
343
452
  select: {
@@ -345,14 +454,16 @@ window.__ModuleLoader__.load({
345
454
  // theme (dark list on dark pages) instead of the OS default
346
455
  // white; option/optgroup styles above reinforce it.
347
456
  colorScheme: "light dark",
348
- background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.1))",
349
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
350
- borderRadius: 8,
351
- 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)",
352
461
  font: "inherit",
353
462
  fontSize: 12,
354
- lineHeight: "20px",
355
- padding: "2px 8px",
463
+ lineHeight: "24px",
464
+ height: 26,
465
+ padding: "0 6px",
466
+ boxSizing: "border-box",
356
467
  maxWidth: 220,
357
468
  cursor: "pointer",
358
469
  flex: "1",
@@ -360,14 +471,16 @@ window.__ModuleLoader__.load({
360
471
  outline: "none"
361
472
  },
362
473
  input: {
363
- background: "transparent",
364
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
365
- 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,
366
477
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
367
478
  font: "inherit",
368
479
  fontSize: 12,
369
- lineHeight: "20px",
370
- padding: "2px 8px",
480
+ lineHeight: "24px",
481
+ height: 26,
482
+ padding: "0 8px",
483
+ boxSizing: "border-box",
371
484
  flex: "1",
372
485
  minWidth: 0,
373
486
  outline: "none"
@@ -376,47 +489,123 @@ window.__ModuleLoader__.load({
376
489
  // silently strips every line feed, so a drafted subject+body would lose
377
490
  // its body before the request was ever sent.
378
491
  textarea: {
379
- background: "transparent",
380
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
381
- 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,
382
495
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
383
496
  font: "inherit",
384
497
  fontSize: 12,
385
- lineHeight: "18px",
498
+ lineHeight: "17px",
386
499
  padding: "4px 8px",
500
+ minHeight: 46,
387
501
  flex: "1",
388
502
  minWidth: 0,
503
+ boxSizing: "border-box",
389
504
  outline: "none",
390
505
  resize: "vertical",
391
506
  overflowY: "auto"
392
507
  },
393
- 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",
394
530
  fontSize: 12,
395
531
  fontWeight: 600,
396
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
397
- margin: "10px 0 4px",
532
+ lineHeight: "18px",
533
+ boxSizing: "border-box",
398
534
  cursor: "pointer",
399
- userSelect: "none"
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,
577
+ cursor: "pointer",
578
+ textAlign: "center"
400
579
  },
401
580
  changeRow: {
402
581
  display: "flex",
403
582
  alignItems: "center",
404
583
  gap: 8,
405
584
  fontSize: 12,
406
- lineHeight: "20px",
407
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
408
- padding: "1px 6px",
409
- margin: "0 -6px",
585
+ lineHeight: "18px",
586
+ padding: "3px 6px",
410
587
  borderRadius: 6,
411
588
  minWidth: 0,
412
589
  cursor: "pointer"
413
590
  },
414
591
  changePath: {
592
+ flex: "1",
593
+ minWidth: 0,
415
594
  overflow: "hidden",
416
595
  textOverflow: "ellipsis",
417
596
  whiteSpace: "nowrap",
418
- flex: "1",
419
- 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)"
420
609
  },
421
610
  // A change row is the diff viewer's navigation: it has to read as a
422
611
  // control, and to show which file the right-hand pane is showing.
@@ -425,14 +614,12 @@ window.__ModuleLoader__.load({
425
614
  alignItems: "center",
426
615
  gap: 8,
427
616
  fontSize: 12,
428
- lineHeight: "20px",
429
- color: "var(--dsw-alias-label-primary, #e5e7eb)",
430
- padding: "1px 6px",
431
- margin: "0 -6px",
617
+ lineHeight: "18px",
618
+ padding: "3px 6px",
432
619
  borderRadius: 6,
433
620
  minWidth: 0,
434
621
  cursor: "pointer",
435
- 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)"
436
623
  },
437
624
  // ── two-pane (changes + diff) layout ────────────────────────────
438
625
  // `bodySplit` replaces `body` while a file is selected: the left pane
@@ -504,9 +691,9 @@ window.__ModuleLoader__.load({
504
691
  lineHeight: "18px",
505
692
  padding: "0 8px",
506
693
  borderRadius: 999,
507
- 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))",
508
695
  background: "transparent",
509
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
696
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
510
697
  font: "inherit",
511
698
  fontSize: 11,
512
699
  cursor: "pointer"
@@ -517,7 +704,7 @@ window.__ModuleLoader__.load({
517
704
  padding: "0 8px",
518
705
  borderRadius: 999,
519
706
  border: "1px solid transparent",
520
- 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)",
521
708
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
522
709
  font: "inherit",
523
710
  fontSize: 11,
@@ -547,7 +734,7 @@ window.__ModuleLoader__.load({
547
734
  width: 34,
548
735
  textAlign: "right",
549
736
  paddingRight: 6,
550
- color: "var(--dsw-alias-label-quaternary, #6b7280)",
737
+ color: "var(--dsw-alias-label-tertiary, #6b7280)",
551
738
  userSelect: "none",
552
739
  WebkitUserSelect: "none"
553
740
  },
@@ -571,7 +758,7 @@ window.__ModuleLoader__.load({
571
758
  color: "var(--dsw-alias-state-business-primary, #4f8cff)"
572
759
  },
573
760
  diffMetaRow: {
574
- color: "var(--dsw-alias-label-quaternary, #6b7280)"
761
+ color: "var(--dsw-alias-label-tertiary, #6b7280)"
575
762
  },
576
763
  diffNotice: {
577
764
  margin: "6px 10px",
@@ -579,7 +766,7 @@ window.__ModuleLoader__.load({
579
766
  borderRadius: 8,
580
767
  fontSize: 12,
581
768
  lineHeight: "18px",
582
- 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))",
583
770
  color: "var(--dsw-alias-label-secondary, #c8ccd2)"
584
771
  },
585
772
  statusTag: {
@@ -588,40 +775,86 @@ window.__ModuleLoader__.load({
588
775
  lineHeight: "16px",
589
776
  padding: "0 6px",
590
777
  borderRadius: 4,
591
- 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)"
592
780
  },
593
781
  logRow: {
594
782
  display: "flex",
595
783
  alignItems: "center",
596
784
  gap: 8,
597
785
  fontSize: 12,
598
- lineHeight: "20px",
599
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
600
- padding: "1px 0",
786
+ lineHeight: "18px",
787
+ padding: "3px 6px",
788
+ borderRadius: 6,
601
789
  minWidth: 0
602
790
  },
603
791
  logSha: {
604
792
  flex: "none",
605
- color: "var(--dsw-alias-state-business-primary, #4f8cff)",
606
- 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)"
607
804
  },
608
805
  output: {
609
- marginTop: 8,
806
+ // Sits at the top of the body, so the breathing room belongs below it.
807
+ // The card itself is one localized summary line tall; git's own words
808
+ // live in `outputDetail`, behind the toggle.
809
+ marginBottom: 8,
610
810
  padding: "6px 10px",
611
811
  borderRadius: 8,
612
812
  fontSize: 12,
813
+ lineHeight: "18px"
814
+ },
815
+ outputLine: {
816
+ display: "flex",
817
+ alignItems: "center",
818
+ gap: 8
819
+ },
820
+ outputSummary: {
821
+ flex: "1",
822
+ minWidth: 0,
823
+ overflow: "hidden",
824
+ textOverflow: "ellipsis",
825
+ whiteSpace: "nowrap"
826
+ },
827
+ outputToggle: {
828
+ flex: "none",
829
+ padding: "0 6px",
830
+ height: 20,
831
+ border: "1px solid currentColor",
832
+ borderRadius: 6,
833
+ background: "transparent",
834
+ color: "inherit",
835
+ font: "inherit",
836
+ fontSize: 11,
613
837
  lineHeight: "18px",
838
+ cursor: "pointer"
839
+ },
840
+ outputDetail: {
841
+ marginTop: 6,
842
+ paddingTop: 6,
843
+ borderTop: "1px solid color-mix(in srgb, currentColor 30%, transparent)",
844
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
845
+ fontSize: 11,
846
+ lineHeight: "16px",
614
847
  wordBreak: "break-word",
615
848
  maxHeight: 160,
616
849
  overflowY: "auto",
617
850
  whiteSpace: "pre-wrap"
618
851
  },
619
852
  outputOk: {
620
- 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)",
621
854
  color: "var(--dsw-alias-state-success-primary, #34c759)"
622
855
  },
623
856
  outputError: {
624
- 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)",
625
858
  color: "var(--dsw-alias-state-error-primary, #ef4444)"
626
859
  },
627
860
  dockWrap: {
@@ -631,7 +864,7 @@ window.__ModuleLoader__.load({
631
864
  maxWidth: "100%",
632
865
  fontSize: 12,
633
866
  lineHeight: "20px",
634
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
867
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
635
868
  whiteSpace: "nowrap",
636
869
  overflow: "hidden",
637
870
  textOverflow: "ellipsis",
@@ -645,9 +878,206 @@ window.__ModuleLoader__.load({
645
878
  return text.length > max ? text.slice(0, max - 1) + "…" : text;
646
879
  }
647
880
 
881
+ /**
882
+ * Strip ANSI/VT escape sequences from git's human-readable output.
883
+ *
884
+ * `git push` forwards the remote's own sideband banner verbatim — Gitee,
885
+ * for one, decorates "Powered by GITEE.COM" with SGR codes, and its first
886
+ * sequence is even truncated (`ESC[0`) right before a complete one. A DOM
887
+ * is not a terminal: the ESC byte has no glyph, so only the parameters
888
+ * survived and the banner read `Powered by [0[01;33mGITEE.COM [0[m…`.
889
+ *
890
+ * Only `message` text passes through here. Diff bodies and paths must not:
891
+ * a file may legitimately contain ESC bytes and the pane shows it verbatim.
892
+ *
893
+ * @param text - a git message string (or anything else).
894
+ * @returns the text with escape sequences removed.
895
+ */
896
+ function stripAnsiEscapes(text) {
897
+ return (text === undefined || text === null ? "" : String(text))
898
+ // OSC (window title / hyperlink): ESC ] … BEL or ESC \
899
+ .replace(/\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)/g, "")
900
+ // A complete CSI sequence: ESC [ params/intermediates final byte
901
+ .replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, "")
902
+ // A truncated CSI (the remote's malformed banner): ESC [ params only
903
+ .replace(/\u001B\[[0-9;?]*/g, "")
904
+ // Any other two-character escape, then any surviving ESC.
905
+ .replace(/\u001B[@-Z\\-_]/g, "")
906
+ .replace(/\u001B/g, "");
907
+ }
908
+
909
+ /** Sanitize one RPC result's human-readable text, leaving data untouched. */
910
+ function sanitizeResult(result) {
911
+ if (result === null || typeof result !== "object") return result;
912
+ var message = function (holder) {
913
+ return holder !== null && typeof holder === "object" && typeof holder.message === "string"
914
+ ? Object.assign({}, holder, { message: stripAnsiEscapes(holder.message) })
915
+ : holder;
916
+ };
917
+ var value = message(result.value);
918
+ var error = message(result.error);
919
+ if (value === result.value && error === result.error) return result;
920
+ return Object.assign({}, result, { value: value, error: error });
921
+ }
922
+
923
+ /** Longest one-line summary an operation card shows before folding. */
924
+ var OUTPUT_SUMMARY_MAX = 160;
925
+
926
+ /**
927
+ * Word one operation result the default way: a summary the panel can
928
+ * localize from `action` (the store holds no translator), plus git's own
929
+ * output as collapsible detail.
930
+ *
931
+ * git's output is often several lines of advice — the LF/CRLF warning on
932
+ * every staged file, a remote's push banner — and showing it as the
933
+ * notification buries the answer to "did it work?". So success keeps git's
934
+ * words behind the toggle, and a failure shows the head of the diagnostic
935
+ * with the whole thing one click away.
936
+ *
937
+ * @param action - the operation name (`push`, `stage`, …).
938
+ * @param res - the raw RPC result.
939
+ * @param params - interpolation for the localized summary (e.g. `{ branch }`).
940
+ * @returns `{ kind, action, params, text, detail }`.
941
+ */
942
+ function summarizeResult(action, res, params) {
943
+ if (res && res.ok === true) {
944
+ var raw = res.value && typeof res.value.message === "string" ? res.value.message.trim() : "";
945
+ return { kind: "ok", action: action, params: params || null, text: null, detail: raw };
946
+ }
947
+ var message = res && res.error && res.error.message ? String(res.error.message).trim() : "";
948
+ return {
949
+ kind: "error",
950
+ action: action,
951
+ params: params || null,
952
+ text: message === "" ? null : shortText(message, OUTPUT_SUMMARY_MAX),
953
+ detail: message.length > OUTPUT_SUMMARY_MAX ? message : ""
954
+ };
955
+ }
956
+
957
+ /**
958
+ * One operation-feedback card: the localized summary line and, when git
959
+ * said anything, git's own output behind a toggle. Pure and exported, so
960
+ * the render test can drive both states (SSR cannot click).
961
+ *
962
+ * @param result - `state.lastResult` (`{ kind, action, params, text, detail }`).
963
+ * @param t - the locale translator.
964
+ * @param open - whether the detail block is expanded.
965
+ * @param onToggle - called when the detail toggle is clicked.
966
+ * @returns the card element, or null without a result.
967
+ */
968
+ function outputView(result, t, open, onToggle) {
969
+ if (!result) return null;
970
+ var summary = result.text !== undefined && result.text !== null && String(result.text) !== ""
971
+ ? String(result.text)
972
+ : result.action
973
+ ? t("output." + result.action, result.params || undefined)
974
+ : t(result.kind === "error" ? "output.failed" : "output.ok");
975
+ var detail = typeof result.detail === "string" ? result.detail.trim() : "";
976
+ var line = [
977
+ h("span", { key: "text", style: S.outputSummary, title: summary }, summary)
978
+ ];
979
+ if (detail !== "") {
980
+ line.push(h("button", {
981
+ key: "toggle",
982
+ type: "button",
983
+ "data-dsh-git": "output-toggle",
984
+ style: S.outputToggle,
985
+ "aria-expanded": open ? "true" : "false",
986
+ onClick: onToggle
987
+ }, (open ? "▴ " : "▾ ") + t(open ? "output.detailHide" : "output.detail")));
988
+ }
989
+ var children = [h("div", { key: "line", style: S.outputLine }, line)];
990
+ if (open && detail !== "") {
991
+ children.push(h("div", { key: "detail", "data-dsh-git": "output-detail", style: S.outputDetail }, detail));
992
+ }
993
+ return h("div", {
994
+ key: "output",
995
+ "data-dsh-git": "output",
996
+ style: Object.assign({}, S.output, result.kind === "ok" ? S.outputOk : S.outputError)
997
+ }, children);
998
+ }
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
+
648
1078
  /** Call one /dsh-git-rpc endpoint. */
649
1079
  function rpc(ctx, endpoint, args) {
650
- return ctx.connection.rpc.call("/dsh-git-rpc", endpoint, { args: args || {} });
1080
+ return ctx.connection.rpc.call("/dsh-git-rpc", endpoint, { args: args || {} }).then(sanitizeResult);
651
1081
  }
652
1082
 
653
1083
  /**
@@ -702,6 +1132,7 @@ window.__ModuleLoader__.load({
702
1132
  commits: [],
703
1133
  error: null,
704
1134
  busy: null,
1135
+ refreshing: false,
705
1136
  lastResult: null,
706
1137
  panelOpen: false
707
1138
  };
@@ -763,7 +1194,19 @@ window.__ModuleLoader__.load({
763
1194
 
764
1195
  function doRefresh(cwd) {
765
1196
  var mySeq = ++seq;
766
- 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
+ }));
767
1210
  if (!cwd) return Promise.resolve();
768
1211
  return Promise.all([
769
1212
  verbs.status(cwd),
@@ -823,17 +1266,20 @@ window.__ModuleLoader__.load({
823
1266
  * Run one action; on success force a fresh load; surfaces lastResult.
824
1267
  * `describe` lets a caller own the wording of both outcomes (used by
825
1268
  * generation, where the success value is the message itself and a
826
- * failure code maps to a localized string). Resolves with the raw
827
- * RPC result so the caller can also act on it.
1269
+ * failure code maps to a localized string). Without it the result is
1270
+ * summarized by {@link summarizeResult}: a localized one-liner plus
1271
+ * git's own output as detail. `params` interpolates that summary (e.g.
1272
+ * `{ branch }`); the panel localizes it, since the store holds no `t`.
1273
+ * Resolves with the raw RPC result so the caller can also act on it.
1274
+ * @param name - the operation name (`push`, `commit`, …).
1275
+ * @param run - performs the call.
1276
+ * @param describe - optional wording override for both outcomes.
1277
+ * @param params - optional interpolation for the default summary.
828
1278
  */
829
- function act(name, run, describe) {
1279
+ function act(name, run, describe, params) {
830
1280
  emit(Object.assign({}, state, { busy: name, lastResult: null }));
831
1281
  return run().then(function (res) {
832
- var result = describe
833
- ? describe(res)
834
- : res && res.ok === true
835
- ? { kind: "ok", text: res.value && res.value.message ? String(res.value.message) : null }
836
- : { kind: "error", text: res && res.error && res.error.message ? String(res.error.message) : "operation failed" };
1282
+ var result = describe ? describe(res) : summarizeResult(name, res, params);
837
1283
  emit(Object.assign({}, state, { busy: null, lastResult: result }));
838
1284
  if (res && res.ok === true) {
839
1285
  var cwd = state.cwd;
@@ -841,10 +1287,10 @@ window.__ModuleLoader__.load({
841
1287
  }
842
1288
  return res;
843
1289
  }).catch(function (error) {
844
- emit(Object.assign({}, state, {
845
- busy: null,
846
- lastResult: { kind: "error", text: error && error.message ? String(error.message) : String(error) }
847
- }));
1290
+ // A transport failure is worded exactly like a git failure: the
1291
+ // same summary/detail split, so the card never grows a second shape.
1292
+ var failure = { ok: false, error: { message: error && error.message ? String(error.message) : String(error) } };
1293
+ emit(Object.assign({}, state, { busy: null, lastResult: summarizeResult(name, failure, params) }));
848
1294
  });
849
1295
  }
850
1296
 
@@ -870,7 +1316,7 @@ window.__ModuleLoader__.load({
870
1316
  };
871
1317
  var OPTGROUP_STYLE = {
872
1318
  background: "var(--dsw-alias-bg-base, #1e1e1e)",
873
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
1319
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
874
1320
  fontWeight: 600
875
1321
  };
876
1322
 
@@ -1021,6 +1467,32 @@ window.__ModuleLoader__.load({
1021
1467
  var PANEL_MARGIN = 24;
1022
1468
  /** Grab width of the panel's right-edge resize handle. */
1023
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
+ }
1024
1496
 
1025
1497
  /** The last path segment, for chip-sized text. */
1026
1498
  function baseName(file) {
@@ -1371,12 +1843,31 @@ window.__ModuleLoader__.load({
1371
1843
  var generateModeRef = React.useState("staged");
1372
1844
  var generateMode = generateModeRef[0];
1373
1845
  var setGenerateMode = generateModeRef[1];
1374
- 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);
1375
1851
  var changesOpen = changesOpenRef[0];
1376
1852
  var setChangesOpen = changesOpenRef[1];
1377
- 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);
1378
1857
  var logOpen = logOpenRef[0];
1379
1858
  var setLogOpen = logOpenRef[1];
1859
+ var logAllRef = React.useState(false);
1860
+ var logAll = logAllRef[0];
1861
+ var setLogAll = logAllRef[1];
1862
+ // Whether the last operation's git output is expanded. A new result
1863
+ // always starts collapsed: the summary line is the notification, the
1864
+ // raw output is opt-in.
1865
+ var outputOpenRef = React.useState(false);
1866
+ var outputOpen = outputOpenRef[0];
1867
+ var setOutputOpen = outputOpenRef[1];
1868
+ React.useEffect(function () {
1869
+ setOutputOpen(false);
1870
+ }, [state.lastResult]);
1380
1871
 
1381
1872
  // Branch the user picked while the tree is dirty: the pre-check
1382
1873
  // shows a warning instead of switching immediately; the switch only
@@ -1629,11 +2120,7 @@ window.__ModuleLoader__.load({
1629
2120
  // while nothing is selected, and the two-pane width (workbench + diff)
1630
2121
  // once a file is open. Dragging the panel's right edge is what resizes
1631
2122
  // the diff, so the panel grows and shrinks with it.
1632
- var panelWidth = selected === null ? S.panel.width : DIFF_LEFT_PANE_WIDTH + PANEL_BORDER_WIDTH + diffWidth;
1633
- var panelStyle = Object.assign({}, S.panel, {
1634
- width: panelWidth,
1635
- maxHeight: selected === null ? S.panel.maxHeight : "72vh"
1636
- });
2123
+ var panelStyle = panelBoxStyle(selected, diffWidth);
1637
2124
  if (dragPos) {
1638
2125
  panelStyle = Object.assign(panelStyle, {
1639
2126
  left: dragPos.left,
@@ -1667,20 +2154,24 @@ window.__ModuleLoader__.load({
1667
2154
  "data-dsh-git": "panel-drag"
1668
2155
  }, [
1669
2156
  h("span", { key: "title", style: S.title }, "⎇ " + t("panel.title")),
1670
- 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)),
1671
2158
  h("button", {
1672
2159
  key: "refresh",
1673
2160
  type: "button",
1674
- style: S.button,
1675
- 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,
1676
2166
  onClick: function () { store.refresh(state.cwd); },
1677
2167
  "aria-label": t("refresh.aria"),
1678
- title: t("refresh.aria")
1679
- }, ""),
2168
+ "aria-busy": state.refreshing === true ? "true" : "false",
2169
+ title: state.refreshing === true ? t("refresh.busy") : t("refresh.aria")
2170
+ }, state.refreshing === true ? "⋯" : "↻"),
1680
2171
  h("button", {
1681
2172
  key: "collapse",
1682
2173
  type: "button",
1683
- style: S.button,
2174
+ style: S.iconButton,
1684
2175
  onClick: function () { store.setPanelOpen(false); },
1685
2176
  "aria-label": t("panel.collapse"),
1686
2177
  title: t("panel.collapse")
@@ -1688,6 +2179,16 @@ window.__ModuleLoader__.load({
1688
2179
  ]);
1689
2180
 
1690
2181
  var bodyChildren = [];
2182
+ // Operation feedback sits at the very TOP of the body. A push banner is
2183
+ // several lines long, and at the bottom of a 400px panel it landed under
2184
+ // the change list and the log — below the fold on any repo with a few
2185
+ // files, which is exactly where nobody looks after clicking a button.
2186
+ if (state.lastResult) {
2187
+ bodyChildren.push(outputView(state.lastResult, t, outputOpen, function () { setOutputOpen(!outputOpen); }));
2188
+ }
2189
+ if (busy) {
2190
+ bodyChildren.push(h("div", { key: "busy", style: S.meta }, busyLabel));
2191
+ }
1691
2192
  if (state.phase === "loading" || state.phase === "idle") {
1692
2193
  bodyChildren.push(h("div", { key: "loading", style: S.meta }, t("loading")));
1693
2194
  } else if (state.phase === "error") {
@@ -1695,15 +2196,21 @@ window.__ModuleLoader__.load({
1695
2196
  } else if (!state.repo) {
1696
2197
  bodyChildren.push(h("div", { key: "norepo", style: S.meta }, t("notRepo")));
1697
2198
  } else {
1698
- // 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.
1699
2202
  var statusLine = [];
1700
2203
  var branchText = state.branch;
1701
2204
  if (!branchText && state.detached) {
1702
2205
  branchText = t("detached") + (state.oid ? " " + state.oid.slice(0, 7) : "");
1703
2206
  }
1704
- 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
+ }
1705
2210
  if (state.dirty > 0) {
1706
- 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")));
1707
2214
  }
1708
2215
  if (state.ahead > 0 || state.behind > 0) {
1709
2216
  statusLine.push(h("span", { key: "ab", style: S.meta }, "↑" + state.ahead + " ↓" + state.behind));
@@ -1728,7 +2235,7 @@ window.__ModuleLoader__.load({
1728
2235
  var value = event.target.value;
1729
2236
  if (!value || value === state.branch) { setPendingBranch(null); return; }
1730
2237
  if (state.dirty > 0) { setPendingBranch(value); return; }
1731
- store.act("checkout", function () { return store.verbs.checkout(state.cwd, value); });
2238
+ store.act("checkout", function () { return store.verbs.checkout(state.cwd, value); }, null, { branch: value });
1732
2239
  },
1733
2240
  "aria-label": t("switch.aria")
1734
2241
  }, selectChildren),
@@ -1799,7 +2306,7 @@ window.__ModuleLoader__.load({
1799
2306
  if (nameText === "" || baseText === "" || busy !== null) return;
1800
2307
  setNewBranchName("");
1801
2308
  setNewBranchOpen(false);
1802
- store.act("createBranch", function () { return store.verbs.createBranch(state.cwd, nameText, baseText); });
2309
+ store.act("createBranch", function () { return store.verbs.createBranch(state.cwd, nameText, baseText); }, null, { branch: nameText });
1803
2310
  }
1804
2311
  }
1805
2312
  if (pendingBranch !== null && state.dirty > 0) {
@@ -1817,100 +2324,102 @@ window.__ModuleLoader__.load({
1817
2324
  onClick: function () {
1818
2325
  var target = pendingBranch;
1819
2326
  setPendingBranch(null);
1820
- store.act("checkout", function () { return store.verbs.checkout(state.cwd, target); });
2327
+ store.act("checkout", function () { return store.verbs.checkout(state.cwd, target); }, null, { branch: target });
1821
2328
  }
1822
2329
  }, busy === "checkout" ? t("busy.checkout") : t("switch.force"))
1823
2330
  ]));
1824
2331
  }
1825
2332
 
1826
- // 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.
1827
2336
  bodyChildren.push(h("div", { key: "net", style: S.row }, [
1828
2337
  h("button", {
1829
2338
  key: "fetch",
1830
2339
  type: "button",
1831
- style: Object.assign({}, S.button, busy !== null ? S.buttonDisabled : null),
2340
+ style: Object.assign({}, S.button, S.grow, busy !== null ? S.buttonDisabled : null),
1832
2341
  disabled: busy !== null,
1833
2342
  onClick: function () { store.act("fetch", function () { return store.verbs.fetch(state.cwd); }); }
1834
2343
  }, busy === "fetch" ? t("busy.fetch") : t("action.fetch")),
1835
2344
  h("button", {
1836
2345
  key: "pull",
1837
2346
  type: "button",
1838
- style: Object.assign({}, S.button, busy !== null ? S.buttonDisabled : null),
2347
+ style: Object.assign({}, S.button, S.grow, busy !== null ? S.buttonDisabled : null),
1839
2348
  disabled: busy !== null,
1840
2349
  onClick: function () { store.act("pull", function () { return store.verbs.pull(state.cwd); }); }
1841
2350
  }, busy === "pull" ? t("busy.pull") : t("action.pull")),
1842
2351
  h("button", {
1843
2352
  key: "push",
1844
2353
  type: "button",
1845
- style: Object.assign({}, S.buttonPrimary, busy !== null ? S.buttonDisabled : null),
2354
+ style: Object.assign({}, S.buttonPrimary, S.grow, busy !== null ? S.buttonDisabled : null),
1846
2355
  disabled: busy !== null,
1847
2356
  onClick: function () { store.act("push", function () { return store.verbs.push(state.cwd); }); }
1848
2357
  }, busy === "push" ? t("busy.push") : t("action.push"))
1849
2358
  ]));
1850
2359
 
1851
- // commit tooling: stage everything, pick the draft basis, draft.
1852
- // Ordering mirrors the real workflow (stage draft commit) and
1853
- // keeps the 400px panel readable by splitting the tool row from the
1854
- // 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.
1855
2363
  var noChanges = state.dirty === 0;
1856
- bodyChildren.push(h("div", { key: "commitTools", style: S.row }, [
1857
- h("button", {
1858
- key: "stage",
1859
- type: "button",
1860
- style: Object.assign({}, S.button, busy !== null || noChanges ? S.buttonDisabled : null),
1861
- disabled: busy !== null || noChanges,
1862
- title: noChanges ? t("changes.none") : t("action.stage"),
1863
- onClick: function () { store.act("stage", function () { return store.verbs.stage(state.cwd); }); }
1864
- }, busy === "stage" ? t("busy.stage") : t("action.stage")),
1865
- h("select", {
1866
- key: "mode",
1867
- style: Object.assign({}, S.select, { flex: "none", width: "auto", maxWidth: 96 }),
1868
- value: generateMode,
1869
- disabled: busy !== null,
1870
- title: t("generate.mode.title"),
1871
- "aria-label": t("generate.mode.title"),
1872
- onChange: function (event) { setGenerateMode(event.target.value); }
1873
- }, [
1874
- h("option", { key: "staged", value: "staged", style: OPTION_STYLE }, t("generate.mode.staged")),
1875
- h("option", { key: "unstaged", value: "unstaged", style: OPTION_STYLE }, t("generate.mode.unstaged")),
1876
- 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"))
1877
2395
  ]),
1878
- h("button", {
1879
- key: "generate",
1880
- type: "button",
1881
- style: Object.assign({}, S.button, busy !== null || noChanges ? S.buttonDisabled : null),
1882
- disabled: busy !== null || noChanges,
1883
- title: noChanges ? t("changes.none") : t("action.generate"),
1884
- onClick: doGenerate
1885
- }, busy === "generate" ? t("busy.generate") : "✨ " + t("action.generate"))
1886
- ]));
1887
-
1888
- bodyChildren.push(h("div", { key: "commit", style: S.row }, [
1889
- h("textarea", {
1890
- key: "msg",
1891
- rows: 2,
1892
- style: S.textarea,
1893
- value: message,
1894
- placeholder: t("commit.placeholder"),
1895
- disabled: busy !== null,
1896
- onChange: function (event) { setMessage(event.target.value); },
1897
- // Enter inserts a newline (a message may have a body); the
1898
- // explicit commit chord is Ctrl/Cmd+Enter.
1899
- onKeyDown: function (event) {
1900
- if (event.key === "Enter" && (event.ctrlKey || event.metaKey) && message.trim() !== "" && busy === null) {
1901
- event.preventDefault();
1902
- 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
+ }
1903
2412
  }
1904
- }
1905
- }),
1906
- h("button", {
1907
- key: "commit",
1908
- type: "button",
1909
- style: Object.assign({}, S.buttonPrimary, busy !== null || message.trim() === "" ? S.buttonDisabled : null),
1910
- disabled: busy !== null || message.trim() === "",
1911
- title: message.trim() === "" ? t("commit.empty") : undefined,
1912
- onClick: doCommit
1913
- }, 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
+ ])
1914
2423
  ]));
1915
2424
 
1916
2425
  function doCommit() {
@@ -1935,6 +2444,7 @@ window.__ModuleLoader__.load({
1935
2444
  "no-provider": t("generate.noProvider"),
1936
2445
  "no-model": t("generate.noModel"),
1937
2446
  "llm-empty": t("generate.empty"),
2447
+ "llm-truncated": t("generate.truncated"),
1938
2448
  "cancelled": t("generate.cancelled")
1939
2449
  };
1940
2450
  var text = known[details.code];
@@ -1961,90 +2471,67 @@ window.__ModuleLoader__.load({
1961
2471
  });
1962
2472
  }
1963
2473
 
1964
- // changes (one row per file) — collapsible, and the diff viewer's
1965
- // navigation: clicking a row opens that file's diff on the right.
1966
- var changesRow;
1967
- if (state.changes.length === 0) {
1968
- changesRow = h("div", { key: "none", style: S.meta }, t("changes.none"));
1969
- } else {
1970
- var shown = changesOpen ? state.changes : state.changes.slice(0, 8);
1971
- changesRow = h("div", { key: "list" }, shown.map(function (change, index) {
1972
- var file = change.file !== undefined ? change.file : change.path;
1973
- var origFile = change.origFile !== undefined ? change.origFile : null;
1974
- var isActive = selected !== null && selected.file === file && selected.origFile === origFile;
1975
- return h("div", {
1976
- key: index,
1977
- style: isActive ? S.changeRowActive : S.changeRow,
1978
- role: "button",
1979
- tabIndex: 0,
1980
- title: t("diff.open"),
1981
- "aria-label": t("diff.open") + " " + change.path,
1982
- "data-dsh-git": "change-row",
1983
- "data-active": isActive === true ? "true" : "false",
1984
- onClick: function () { selectChange(change); },
1985
- onKeyDown: function (event) {
1986
- if (event.key === "Enter" || event.key === " ") {
1987
- if (event.preventDefault) event.preventDefault();
1988
- selectChange(change);
1989
- }
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);
1990
2496
  }
1991
- }, [
1992
- h("span", { key: "tag", style: S.statusTag }, t("status." + change.status)),
1993
- h("span", { key: "path", style: S.changePath, title: change.path }, change.path)
1994
- ]);
1995
- }));
1996
- if (state.changes.length > shown.length) {
1997
- changesRow = h("div", { key: "list" }, [
1998
- changesRow,
1999
- h("button", {
2000
- key: "more",
2001
- type: "button",
2002
- style: S.button,
2003
- onClick: function () { setChangesOpen(!changesOpen); }
2004
- }, changesOpen ? "▴" : " " + (state.changes.length - shown.length))
2005
- ]);
2006
- }
2007
- }
2008
- bodyChildren.push(h("div", { key: "changes", style: S.sectionTitle, onClick: function () { setChangesOpen(!changesOpen); } }, (changesOpen ? "▾ " : "▸ ") + t("changes.title", { count: state.changes.length })));
2009
- bodyChildren.push(h("div", { key: "changesBody" }, changesRow));
2010
-
2011
- // recent commits collapsible
2012
- var logRows;
2013
- if (state.commits.length === 0) {
2014
- logRows = h("div", { key: "none", style: S.meta }, t("log.none"));
2015
- } else {
2016
- var logShown = logOpen ? state.commits : state.commits.slice(0, 5);
2017
- logRows = h("div", { key: "list" }, logShown.map(function (commit, index) {
2018
- return h("div", { key: index, style: S.logRow, title: commit.subject }, [
2019
- h("span", { key: "sha", style: S.logSha }, commit.sha),
2020
- h("span", { key: "subj", style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: "1", minWidth: 0 } }, commit.subject)
2021
- ]);
2022
- }));
2023
- if (state.commits.length > logShown.length) {
2024
- logRows = h("div", { key: "list" }, [
2025
- logRows,
2026
- h("button", {
2027
- key: "more",
2028
- type: "button",
2029
- style: S.button,
2030
- onClick: function () { setLogOpen(!logOpen); }
2031
- }, logOpen ? "▴" : "▾ " + (state.commits.length - logShown.length))
2032
- ]);
2033
- }
2034
- }
2035
- bodyChildren.push(h("div", { key: "logTitle", style: S.sectionTitle, onClick: function () { setLogOpen(!logOpen); } }, (logOpen ? "▾ " : "▸ ") + t("log.title")));
2036
- bodyChildren.push(h("div", { key: "logBody" }, logRows));
2037
- }
2038
-
2039
- // last operation output
2040
- if (state.lastResult) {
2041
- bodyChildren.push(h("div", {
2042
- key: "output",
2043
- style: Object.assign({}, S.output, state.lastResult.kind === "ok" ? S.outputOk : S.outputError)
2044
- }, state.lastResult.text || t("output.ok")));
2045
- }
2046
- if (busy) {
2047
- bodyChildren.push(h("div", { key: "busy", style: S.meta }, busyLabel));
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));
2048
2535
  }
2049
2536
 
2050
2537
  // Everything the open diff depends on, as one comparable string: a
@@ -2212,7 +2699,7 @@ window.__ModuleLoader__.load({
2212
2699
  maxWidth: 220,
2213
2700
  padding: "1px 10px",
2214
2701
  borderRadius: 999,
2215
- 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))",
2216
2703
  background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.12))",
2217
2704
  color: "var(--dsw-alias-label-secondary, #c8ccd2)",
2218
2705
  font: "inherit",
@@ -2279,7 +2766,13 @@ window.__ModuleLoader__.load({
2279
2766
  parseDiffSide: parseDiffSide,
2280
2767
  diffRowView: diffRowView,
2281
2768
  GitDiffPane: GitDiffPane,
2282
- DIFF_ROW: DIFF_ROW
2769
+ DIFF_ROW: DIFF_ROW,
2770
+ stripAnsiEscapes: stripAnsiEscapes,
2771
+ outputView: outputView,
2772
+ sectionView: sectionView,
2773
+ changeTagStyle: changeTagStyle,
2774
+ panelBoxStyle: panelBoxStyle,
2775
+ DIFF_PANEL_HEIGHT: DIFF_PANEL_HEIGHT
2283
2776
  };
2284
2777
  return module.exports;
2285
2778
  }