@xia-sc/dsh-git 0.5.3 → 0.6.1

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 拒绝,请先提交或暂存",
@@ -75,10 +76,16 @@ window.__ModuleLoader__.load({
75
76
  "generate.truncated": "模型还没写出提交信息就用完了 token 上限(思考模型会先花掉这份预算),再点一次通常即可",
76
77
  "generate.cancelled": "生成已取消",
77
78
  "generate.failed": "生成失败",
79
+ "generate.failedDetail": "生成失败:{message}",
80
+ "generate.failedRoute": "生成失败:{message}(模型路由 {provider} / {model})",
78
81
  "commit.placeholder": "提交信息(可多行,Ctrl+Enter 提交)…",
79
82
  "commit.empty": "请输入提交信息",
80
- "changes.title": "变更({count})",
83
+ "changes.title": "变更",
81
84
  "changes.none": "工作区干净",
85
+ // Section footer: the list previews its first N rows and this says how
86
+ // many more there are, instead of a bare "▾ 13".
87
+ "section.more": "显示全部 {count} 项",
88
+ "section.less": "只看前几项",
82
89
  "diff.title": "差异",
83
90
  "diff.open": "查看差异",
84
91
  "diff.close": "关闭差异",
@@ -137,6 +144,7 @@ window.__ModuleLoader__.load({
137
144
  "panel.title": "Git",
138
145
  "panel.collapse": "Collapse",
139
146
  "refresh.aria": "Refresh Git status",
147
+ "refresh.busy": "Refreshing…",
140
148
  "branch.aria": "Current git branch",
141
149
  "switch.aria": "Switch branch",
142
150
  "switch.dirtyWarn": "{count} uncommitted file(s) (e.g. {paths}) may block the switch — commit or stash first",
@@ -174,10 +182,14 @@ window.__ModuleLoader__.load({
174
182
  "generate.truncated": "The model hit its token cap before writing anything (a reasoning model spends that budget thinking first) — trying again usually works",
175
183
  "generate.cancelled": "Draft cancelled",
176
184
  "generate.failed": "Draft failed",
185
+ "generate.failedDetail": "Draft failed: {message}",
186
+ "generate.failedRoute": "Draft failed: {message} (route {provider} / {model})",
177
187
  "commit.placeholder": "Commit message (multi-line allowed, Ctrl+Enter commits)…",
178
188
  "commit.empty": "Enter a commit message",
179
- "changes.title": "Changes ({count})",
189
+ "changes.title": "Changes",
180
190
  "changes.none": "Working tree clean",
191
+ "section.more": "Show all {count}",
192
+ "section.less": "Show fewer",
181
193
  "diff.title": "Diff",
182
194
  "diff.open": "View diff",
183
195
  "diff.close": "Close diff",
@@ -243,7 +255,12 @@ window.__ModuleLoader__.load({
243
255
  panel: {
244
256
  position: "fixed",
245
257
  // Sit above the composer card so the expanded panel never covers
246
- // the input box (the input.left pill sits in the tool row).
258
+ // the input box. 168px is a measurement of the composer stack
259
+ // (card + tool row + the dock band), not a published contract:
260
+ // ui-conversation keeps that geometry in its own module CSS
261
+ // (`--dsh-composer-stack-gap` etc.). If the composer grows or
262
+ // shrinks, re-measure here and in DIFF_PANEL_HEIGHT below — the
263
+ // panel is anchored by `bottom`, so both move together.
247
264
  bottom: 168,
248
265
  left: "50%",
249
266
  transform: "translateX(-50%)",
@@ -269,8 +286,8 @@ window.__ModuleLoader__.load({
269
286
  display: "flex",
270
287
  alignItems: "center",
271
288
  gap: 8,
272
- padding: "8px 12px",
273
- borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2))",
289
+ padding: "8px 10px",
290
+ borderBottom: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.22))",
274
291
  flex: "none",
275
292
  cursor: "move",
276
293
  userSelect: "none",
@@ -284,11 +301,39 @@ window.__ModuleLoader__.load({
284
301
  fontSize: 13,
285
302
  fontWeight: 600,
286
303
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
304
+ flex: "none",
305
+ whiteSpace: "nowrap"
306
+ },
307
+ // The workspace path takes the header's slack: it is the one field whose
308
+ // truncation must never move the buttons beside it.
309
+ cwdPath: {
287
310
  flex: "1",
288
311
  minWidth: 0,
289
312
  overflow: "hidden",
290
313
  textOverflow: "ellipsis",
291
- whiteSpace: "nowrap"
314
+ whiteSpace: "nowrap",
315
+ fontSize: 11,
316
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
317
+ textAlign: "right"
318
+ },
319
+ // Square ghost icon button: the panel's own chrome (refresh / collapse).
320
+ iconButton: {
321
+ flex: "none",
322
+ display: "inline-flex",
323
+ alignItems: "center",
324
+ justifyContent: "center",
325
+ width: 24,
326
+ height: 24,
327
+ padding: 0,
328
+ border: "1px solid transparent",
329
+ borderRadius: 6,
330
+ background: "transparent",
331
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
332
+ font: "inherit",
333
+ fontSize: 13,
334
+ lineHeight: "20px",
335
+ boxSizing: "border-box",
336
+ cursor: "pointer"
292
337
  },
293
338
  body: {
294
339
  flex: "1",
@@ -299,9 +344,36 @@ window.__ModuleLoader__.load({
299
344
  row: {
300
345
  display: "flex",
301
346
  alignItems: "center",
302
- gap: 8,
347
+ gap: 6,
303
348
  margin: "6px 0"
304
349
  },
350
+ // One action row inside a card: the gap is owned by the card, not the row.
351
+ rowTight: {
352
+ display: "flex",
353
+ alignItems: "center",
354
+ gap: 6,
355
+ minWidth: 0
356
+ },
357
+ // Equal-width action buttons: a row of auto-width labels was the messy
358
+ // part of the old layout — every group now shares the row evenly.
359
+ grow: {
360
+ flex: "1 1 0",
361
+ minWidth: 0,
362
+ padding: "0 6px",
363
+ textAlign: "center"
364
+ },
365
+ // Card grouping the whole commit flow (stage → draft → message → commit).
366
+ card: {
367
+ margin: "6px 0",
368
+ padding: 8,
369
+ display: "flex",
370
+ flexDirection: "column",
371
+ gap: 6,
372
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.22))",
373
+ borderRadius: 8,
374
+ background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))",
375
+ minWidth: 0
376
+ },
305
377
  warn: {
306
378
  display: "flex",
307
379
  alignItems: "center",
@@ -309,18 +381,22 @@ window.__ModuleLoader__.load({
309
381
  margin: "6px 0",
310
382
  padding: "6px 8px",
311
383
  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)",
384
+ background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0a020) 14%, transparent)",
385
+ border: "1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0a020) 40%, transparent)",
386
+ color: "var(--dsw-alias-state-warn-primary, #f0a020)",
315
387
  fontSize: 12,
316
388
  lineHeight: "16px"
317
389
  },
390
+ // The branch chip — only for the detached-HEAD notice, so it carries the
391
+ // warning colour rather than the accent.
318
392
  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,
393
+ flex: "none",
394
+ maxWidth: "62%",
322
395
  padding: "0 8px",
323
- fontWeight: 500,
396
+ borderRadius: 6,
397
+ background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0a020) 16%, transparent)",
398
+ color: "var(--dsw-alias-state-warn-primary, #f0a020)",
399
+ fontWeight: 600,
324
400
  fontSize: 12,
325
401
  lineHeight: "20px",
326
402
  overflow: "hidden",
@@ -328,40 +404,58 @@ window.__ModuleLoader__.load({
328
404
  whiteSpace: "nowrap"
329
405
  },
330
406
  dirty: {
407
+ flex: "none",
331
408
  color: "var(--dsw-alias-state-warn-primary, #f0a020)",
332
409
  fontWeight: 600,
333
410
  fontSize: 12
334
411
  },
335
412
  meta: {
336
- color: "var(--dsw-alias-label-quaternary, #6b7280)",
337
- fontSize: 12
413
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
414
+ fontSize: 11
338
415
  },
339
416
  button: {
340
- background: "var(--dsw-alias-interactive-bg-hover-solid, rgba(127,127,127,0.14))",
417
+ background: "color-mix(in srgb, var(--dsw-alias-label-primary, #e5e7eb) 8%, transparent)",
341
418
  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)",
419
+ borderRadius: 7,
420
+ color: "var(--dsw-alias-label-primary, #e5e7eb)",
344
421
  font: "inherit",
345
422
  fontSize: 12,
346
- lineHeight: "22px",
423
+ lineHeight: "24px",
424
+ height: 26,
347
425
  padding: "0 10px",
426
+ boxSizing: "border-box",
348
427
  cursor: "pointer",
349
- flex: "none"
428
+ flex: "none",
429
+ whiteSpace: "nowrap",
430
+ overflow: "hidden",
431
+ textOverflow: "ellipsis"
350
432
  },
351
433
  buttonPrimary: {
352
- background: "var(--dsw-alias-state-business-primary, #4f8cff)",
434
+ // The harness's own primary button: the label token IS the fill, with
435
+ // `label-primary-foreground` as the text on top. It reads near-black on
436
+ // light and near-white on dark (an inverted contrast button).
437
+ // `--dsw-alias-brand-primary` is that same contrast colour, NOT an
438
+ // accent — pairing it with `#fff` text made 推送 a white block with
439
+ // invisible text in the dark theme, and 提交 an empty grey box.
440
+ background: "var(--dsw-alias-button-primary-fill, var(--dsw-alias-label-primary, #e5e7eb))",
353
441
  border: "1px solid transparent",
354
- borderRadius: 8,
355
- color: "#fff",
442
+ borderRadius: 7,
443
+ color: "var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-base, #1e1e1e))",
356
444
  font: "inherit",
357
445
  fontSize: 12,
358
- lineHeight: "22px",
446
+ fontWeight: 600,
447
+ lineHeight: "24px",
448
+ height: 26,
359
449
  padding: "0 10px",
450
+ boxSizing: "border-box",
360
451
  cursor: "pointer",
361
- flex: "none"
452
+ flex: "none",
453
+ whiteSpace: "nowrap",
454
+ overflow: "hidden",
455
+ textOverflow: "ellipsis"
362
456
  },
363
457
  buttonDisabled: {
364
- opacity: 0.5,
458
+ opacity: 0.4,
365
459
  cursor: "default"
366
460
  },
367
461
  select: {
@@ -369,14 +463,16 @@ window.__ModuleLoader__.load({
369
463
  // theme (dark list on dark pages) instead of the OS default
370
464
  // white; option/optgroup styles above reinforce it.
371
465
  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)",
466
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.1))",
467
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
468
+ borderRadius: 7,
469
+ color: "var(--dsw-alias-label-primary, #e5e7eb)",
376
470
  font: "inherit",
377
471
  fontSize: 12,
378
- lineHeight: "20px",
379
- padding: "2px 8px",
472
+ lineHeight: "24px",
473
+ height: 26,
474
+ padding: "0 6px",
475
+ boxSizing: "border-box",
380
476
  maxWidth: 220,
381
477
  cursor: "pointer",
382
478
  flex: "1",
@@ -384,14 +480,16 @@ window.__ModuleLoader__.load({
384
480
  outline: "none"
385
481
  },
386
482
  input: {
387
- background: "transparent",
388
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
389
- borderRadius: 8,
483
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.1))",
484
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
485
+ borderRadius: 7,
390
486
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
391
487
  font: "inherit",
392
488
  fontSize: 12,
393
- lineHeight: "20px",
394
- padding: "2px 8px",
489
+ lineHeight: "24px",
490
+ height: 26,
491
+ padding: "0 8px",
492
+ boxSizing: "border-box",
395
493
  flex: "1",
396
494
  minWidth: 0,
397
495
  outline: "none"
@@ -400,47 +498,123 @@ window.__ModuleLoader__.load({
400
498
  // silently strips every line feed, so a drafted subject+body would lose
401
499
  // its body before the request was ever sent.
402
500
  textarea: {
403
- background: "transparent",
404
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
405
- borderRadius: 8,
501
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.1))",
502
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
503
+ borderRadius: 7,
406
504
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
407
505
  font: "inherit",
408
506
  fontSize: 12,
409
- lineHeight: "18px",
507
+ lineHeight: "17px",
410
508
  padding: "4px 8px",
509
+ minHeight: 46,
411
510
  flex: "1",
412
511
  minWidth: 0,
512
+ boxSizing: "border-box",
413
513
  outline: "none",
414
514
  resize: "vertical",
415
515
  overflowY: "auto"
416
516
  },
417
- sectionTitle: {
517
+ // ── list sections (changes / recent commits) ─────────────────────
518
+ // A section is a card: a full-width header toggle (chevron + label +
519
+ // count) and its rows. Collapsing hides the rows outright — the old
520
+ // header doubled as "show more" for a preview, so its chevron pointed
521
+ // the wrong way for a list that was already on screen.
522
+ section: {
523
+ marginTop: 8,
524
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.22))",
525
+ borderRadius: 8,
526
+ background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))",
527
+ overflow: "hidden"
528
+ },
529
+ sectionHeader: {
530
+ display: "flex",
531
+ alignItems: "center",
532
+ gap: 6,
533
+ width: "100%",
534
+ padding: "6px 8px",
535
+ background: "transparent",
536
+ border: "none",
537
+ color: "var(--dsw-alias-label-primary, #e5e7eb)",
538
+ font: "inherit",
418
539
  fontSize: 12,
419
540
  fontWeight: 600,
420
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
421
- margin: "10px 0 4px",
541
+ lineHeight: "18px",
542
+ boxSizing: "border-box",
543
+ cursor: "pointer",
544
+ textAlign: "left"
545
+ },
546
+ sectionChevron: {
547
+ flex: "none",
548
+ width: 10,
549
+ fontSize: 10,
550
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
551
+ },
552
+ sectionLabel: {
553
+ flex: "1",
554
+ minWidth: 0,
555
+ overflow: "hidden",
556
+ textOverflow: "ellipsis",
557
+ whiteSpace: "nowrap"
558
+ },
559
+ sectionCount: {
560
+ flex: "none",
561
+ padding: "0 6px",
562
+ borderRadius: 999,
563
+ background: "color-mix(in srgb, var(--dsw-alias-label-primary, #e5e7eb) 10%, transparent)",
564
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
565
+ fontSize: 11,
566
+ fontWeight: 500,
567
+ lineHeight: "16px"
568
+ },
569
+ sectionBody: {
570
+ padding: "0 4px 4px"
571
+ },
572
+ sectionEmpty: {
573
+ padding: "2px 6px 6px",
574
+ fontSize: 12,
575
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
576
+ },
577
+ sectionMore: {
578
+ display: "block",
579
+ width: "100%",
580
+ padding: "2px 0 4px",
581
+ background: "transparent",
582
+ border: "none",
583
+ color: "var(--dsw-alias-link, #4f8cff)",
584
+ font: "inherit",
585
+ fontSize: 11,
422
586
  cursor: "pointer",
423
- userSelect: "none"
587
+ textAlign: "center"
424
588
  },
425
589
  changeRow: {
426
590
  display: "flex",
427
591
  alignItems: "center",
428
592
  gap: 8,
429
593
  fontSize: 12,
430
- lineHeight: "20px",
431
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
432
- padding: "1px 6px",
433
- margin: "0 -6px",
594
+ lineHeight: "18px",
595
+ padding: "3px 6px",
434
596
  borderRadius: 6,
435
597
  minWidth: 0,
436
598
  cursor: "pointer"
437
599
  },
438
600
  changePath: {
601
+ flex: "1",
602
+ minWidth: 0,
439
603
  overflow: "hidden",
440
604
  textOverflow: "ellipsis",
441
605
  whiteSpace: "nowrap",
442
- flex: "1",
443
- minWidth: 0
606
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
607
+ color: "var(--dsw-alias-label-primary, #e5e7eb)"
608
+ },
609
+ // Per-status chip; `color-mix(currentColor)` gives every status the same
610
+ // tint shape from whichever state colour it carries.
611
+ changeTag: {
612
+ flex: "none",
613
+ padding: "0 6px",
614
+ borderRadius: 4,
615
+ fontSize: 11,
616
+ lineHeight: "16px",
617
+ background: "color-mix(in srgb, currentColor 15%, transparent)"
444
618
  },
445
619
  // A change row is the diff viewer's navigation: it has to read as a
446
620
  // control, and to show which file the right-hand pane is showing.
@@ -449,14 +623,12 @@ window.__ModuleLoader__.load({
449
623
  alignItems: "center",
450
624
  gap: 8,
451
625
  fontSize: 12,
452
- lineHeight: "20px",
453
- color: "var(--dsw-alias-label-primary, #e5e7eb)",
454
- padding: "1px 6px",
455
- margin: "0 -6px",
626
+ lineHeight: "18px",
627
+ padding: "3px 6px",
456
628
  borderRadius: 6,
457
629
  minWidth: 0,
458
630
  cursor: "pointer",
459
- background: "var(--dsw-alias-interactive-bg-hover-solid, rgba(127,127,127,0.18))"
631
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4f8cff) 15%, transparent)"
460
632
  },
461
633
  // ── two-pane (changes + diff) layout ────────────────────────────
462
634
  // `bodySplit` replaces `body` while a file is selected: the left pane
@@ -528,9 +700,9 @@ window.__ModuleLoader__.load({
528
700
  lineHeight: "18px",
529
701
  padding: "0 8px",
530
702
  borderRadius: 999,
531
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
703
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.25))",
532
704
  background: "transparent",
533
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
705
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
534
706
  font: "inherit",
535
707
  fontSize: 11,
536
708
  cursor: "pointer"
@@ -541,7 +713,7 @@ window.__ModuleLoader__.load({
541
713
  padding: "0 8px",
542
714
  borderRadius: 999,
543
715
  border: "1px solid transparent",
544
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.18))",
716
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4f8cff) 16%, transparent)",
545
717
  color: "var(--dsw-alias-label-primary, #e5e7eb)",
546
718
  font: "inherit",
547
719
  fontSize: 11,
@@ -571,7 +743,7 @@ window.__ModuleLoader__.load({
571
743
  width: 34,
572
744
  textAlign: "right",
573
745
  paddingRight: 6,
574
- color: "var(--dsw-alias-label-quaternary, #6b7280)",
746
+ color: "var(--dsw-alias-label-tertiary, #6b7280)",
575
747
  userSelect: "none",
576
748
  WebkitUserSelect: "none"
577
749
  },
@@ -595,7 +767,7 @@ window.__ModuleLoader__.load({
595
767
  color: "var(--dsw-alias-state-business-primary, #4f8cff)"
596
768
  },
597
769
  diffMetaRow: {
598
- color: "var(--dsw-alias-label-quaternary, #6b7280)"
770
+ color: "var(--dsw-alias-label-tertiary, #6b7280)"
599
771
  },
600
772
  diffNotice: {
601
773
  margin: "6px 10px",
@@ -603,7 +775,7 @@ window.__ModuleLoader__.load({
603
775
  borderRadius: 8,
604
776
  fontSize: 12,
605
777
  lineHeight: "18px",
606
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.12))",
778
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.12))",
607
779
  color: "var(--dsw-alias-label-secondary, #c8ccd2)"
608
780
  },
609
781
  statusTag: {
@@ -612,22 +784,32 @@ window.__ModuleLoader__.load({
612
784
  lineHeight: "16px",
613
785
  padding: "0 6px",
614
786
  borderRadius: 4,
615
- background: "var(--dsw-alias-surface-tertiary, rgba(127,127,127,0.12))"
787
+ background: "color-mix(in srgb, var(--dsw-alias-label-primary, #e5e7eb) 10%, transparent)",
788
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
616
789
  },
617
790
  logRow: {
618
791
  display: "flex",
619
792
  alignItems: "center",
620
793
  gap: 8,
621
794
  fontSize: 12,
622
- lineHeight: "20px",
623
- color: "var(--dsw-alias-label-secondary, #c8ccd2)",
624
- padding: "1px 0",
795
+ lineHeight: "18px",
796
+ padding: "3px 6px",
797
+ borderRadius: 6,
625
798
  minWidth: 0
626
799
  },
627
800
  logSha: {
628
801
  flex: "none",
629
- color: "var(--dsw-alias-state-business-primary, #4f8cff)",
630
- fontFamily: "monospace"
802
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
803
+ fontSize: 11,
804
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)"
805
+ },
806
+ logSubject: {
807
+ flex: "1",
808
+ minWidth: 0,
809
+ overflow: "hidden",
810
+ textOverflow: "ellipsis",
811
+ whiteSpace: "nowrap",
812
+ color: "var(--dsw-alias-label-primary, #e5e7eb)"
631
813
  },
632
814
  output: {
633
815
  // Sits at the top of the body, so the breathing room belongs below it.
@@ -667,8 +849,8 @@ window.__ModuleLoader__.load({
667
849
  outputDetail: {
668
850
  marginTop: 6,
669
851
  paddingTop: 6,
670
- borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,0.35))",
671
- fontFamily: "monospace",
852
+ borderTop: "1px solid color-mix(in srgb, currentColor 30%, transparent)",
853
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
672
854
  fontSize: 11,
673
855
  lineHeight: "16px",
674
856
  wordBreak: "break-word",
@@ -677,11 +859,11 @@ window.__ModuleLoader__.load({
677
859
  whiteSpace: "pre-wrap"
678
860
  },
679
861
  outputOk: {
680
- background: "var(--dsw-alias-state-success-tertiary, rgba(52,199,89,0.12))",
862
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary, #34c759) 14%, transparent)",
681
863
  color: "var(--dsw-alias-state-success-primary, #34c759)"
682
864
  },
683
865
  outputError: {
684
- background: "var(--dsw-alias-state-error-tertiary, rgba(239,68,68,0.12))",
866
+ background: "color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 14%, transparent)",
685
867
  color: "var(--dsw-alias-state-error-primary, #ef4444)"
686
868
  },
687
869
  dockWrap: {
@@ -691,7 +873,7 @@ window.__ModuleLoader__.load({
691
873
  maxWidth: "100%",
692
874
  fontSize: 12,
693
875
  lineHeight: "20px",
694
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
876
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
695
877
  whiteSpace: "nowrap",
696
878
  overflow: "hidden",
697
879
  textOverflow: "ellipsis",
@@ -705,6 +887,33 @@ window.__ModuleLoader__.load({
705
887
  return text.length > max ? text.slice(0, max - 1) + "…" : text;
706
888
  }
707
889
 
890
+ /**
891
+ * The model route one session's AI draft should use, or null to let the
892
+ * host fall back to its first registered provider route.
893
+ *
894
+ * Two host generations carry the same `modelSelection` projection: the
895
+ * row's own `projectionValues` (present since 0.1.6) and, since 0.1.7,
896
+ * the snapshot's per-session `projectionsBySession[id].values`. Read the
897
+ * row first, then the shared record, so neither generation loses a route
898
+ * the user already chose. `next` (a pending pick) beats `lastUsed`.
899
+ *
900
+ * @param summary - `byId[sessionId]` row, when the snapshot has one.
901
+ * @param sessions - the whole `useSessions` snapshot (may be undefined).
902
+ * @param sessionId - the bound session id, or null/undefined.
903
+ * @returns `{ provider, model }`, or null.
904
+ */
905
+ function sessionModelRoute(summary, sessions, sessionId) {
906
+ var selection = summary && summary.projectionValues ? summary.projectionValues.modelSelection : undefined;
907
+ if (!selection && sessionId !== null && sessionId !== undefined && sessions && sessions.projectionsBySession) {
908
+ var record = sessions.projectionsBySession[sessionId];
909
+ selection = record && record.values ? record.values.modelSelection : undefined;
910
+ }
911
+ if (!selection) return null;
912
+ var effective = selection.next || selection.lastUsed;
913
+ if (!effective || !effective.provider || !effective.model) return null;
914
+ return { provider: effective.provider, model: effective.model };
915
+ }
916
+
708
917
  /**
709
918
  * Strip ANSI/VT escape sequences from git's human-readable output.
710
919
  *
@@ -824,6 +1033,84 @@ window.__ModuleLoader__.load({
824
1033
  }, children);
825
1034
  }
826
1035
 
1036
+ /** Rows a collapsed-by-default list shows before its "show all" footer. */
1037
+ var CHANGES_PREVIEW = 8;
1038
+ /** Same, for the recent-commit list (shorter rows, less to say). */
1039
+ var LOG_PREVIEW = 5;
1040
+
1041
+ /** The state colour one porcelain status carries, in theme tokens. */
1042
+ function changeTagStyle(status) {
1043
+ var color = status === "added" || status === "untracked"
1044
+ ? "var(--dsw-alias-state-success-primary, #34c759)"
1045
+ : status === "deleted" || status === "conflict"
1046
+ ? "var(--dsw-alias-state-error-primary, #ef4444)"
1047
+ : status === "renamed" || status === "copied"
1048
+ // `state-business-primary` is the real accent (deepseek blue);
1049
+ // `brand-primary` is the inverted contrast colour and would make
1050
+ // this chip white-on-white in the dark theme.
1051
+ ? "var(--dsw-alias-state-business-primary, #4f8cff)"
1052
+ : status === "modified" || status === "changed"
1053
+ ? "var(--dsw-alias-state-warn-primary, #f0a020)"
1054
+ : "var(--dsw-alias-label-secondary, #c8ccd2)";
1055
+ // `changeTag`'s background is `color-mix(currentColor…)`, so the tint
1056
+ // follows whichever colour the status carries.
1057
+ return Object.assign({}, S.changeTag, { color: color });
1058
+ }
1059
+
1060
+ /**
1061
+ * One collapsible list section: a full-width header toggle (chevron +
1062
+ * label + count) and, while open, its rows plus a footer that expands the
1063
+ * preview. Two separate states on purpose — the header collapses the
1064
+ * section, the footer widens it — because one flag doing both made the
1065
+ * chevron point the wrong way for a list that was already on screen.
1066
+ *
1067
+ * @param spec - `{ hook, label, count, open, all, cap, empty, rows, onToggle, onMore }`.
1068
+ * @param t - the locale translator.
1069
+ * @returns the section element.
1070
+ */
1071
+ function sectionView(spec, t) {
1072
+ var children = [h("button", {
1073
+ key: "header",
1074
+ type: "button",
1075
+ "data-dsh-git": spec.hook + "-header",
1076
+ style: S.sectionHeader,
1077
+ "aria-expanded": spec.open ? "true" : "false",
1078
+ onClick: spec.onToggle
1079
+ }, [
1080
+ h("span", { key: "chevron", style: S.sectionChevron }, spec.open ? "▾" : "▸"),
1081
+ h("span", { key: "label", style: S.sectionLabel }, spec.label),
1082
+ h("span", { key: "count", style: S.sectionCount }, String(spec.count))
1083
+ ])];
1084
+ if (spec.open) {
1085
+ var previewed = spec.all || spec.count <= spec.cap;
1086
+ children.push(h("div", {
1087
+ key: "body",
1088
+ "data-dsh-git": spec.hook + "-body",
1089
+ style: S.sectionBody
1090
+ }, spec.count === 0
1091
+ ? h("div", { key: "empty", style: S.sectionEmpty }, spec.empty)
1092
+ : (previewed ? spec.rows : spec.rows.slice(0, spec.cap))));
1093
+ if (!previewed) {
1094
+ children.push(h("button", {
1095
+ key: "more",
1096
+ type: "button",
1097
+ "data-dsh-git": spec.hook + "-more",
1098
+ style: S.sectionMore,
1099
+ onClick: spec.onMore
1100
+ }, t("section.more", { count: spec.count })));
1101
+ } else if (spec.all && spec.count > spec.cap) {
1102
+ children.push(h("button", {
1103
+ key: "more",
1104
+ type: "button",
1105
+ "data-dsh-git": spec.hook + "-more",
1106
+ style: S.sectionMore,
1107
+ onClick: spec.onMore
1108
+ }, t("section.less")));
1109
+ }
1110
+ }
1111
+ return h("div", { key: spec.hook, "data-dsh-git": spec.hook, style: S.section }, children);
1112
+ }
1113
+
827
1114
  /** Call one /dsh-git-rpc endpoint. */
828
1115
  function rpc(ctx, endpoint, args) {
829
1116
  return ctx.connection.rpc.call("/dsh-git-rpc", endpoint, { args: args || {} }).then(sanitizeResult);
@@ -849,8 +1136,11 @@ window.__ModuleLoader__.load({
849
1136
  commit: function (cwd, message) { return rpc(ctx, "commit", { cwd: cwd, message: message }); },
850
1137
  push: function (cwd) { return rpc(ctx, "push", { cwd: cwd }); },
851
1138
  log: function (cwd) { return rpc(ctx, "log", { cwd: cwd, count: 10 }); },
852
- generateMessage: function (cwd, mode, provider, model) {
853
- return rpc(ctx, "generateMessage", { cwd: cwd, mode: mode, provider: provider, model: model });
1139
+ generateMessage: function (cwd, mode, provider, model, sessionId) {
1140
+ // The session travels with the request: session-labelled
1141
+ // providers reject an unidentified call, and the host forwards
1142
+ // a session id to its adapter only when one is present.
1143
+ return rpc(ctx, "generateMessage", { cwd: cwd, mode: mode, provider: provider, model: model, sessionId: sessionId });
854
1144
  }
855
1145
  };
856
1146
 
@@ -881,6 +1171,7 @@ window.__ModuleLoader__.load({
881
1171
  commits: [],
882
1172
  error: null,
883
1173
  busy: null,
1174
+ refreshing: false,
884
1175
  lastResult: null,
885
1176
  panelOpen: false
886
1177
  };
@@ -942,7 +1233,19 @@ window.__ModuleLoader__.load({
942
1233
 
943
1234
  function doRefresh(cwd) {
944
1235
  var mySeq = ++seq;
945
- emit(Object.assign({}, state, { sessionId: sessionId, cwd: cwd, phase: cwd ? "loading" : "idle", error: null }));
1236
+ // Re-reading the SAME workspace keeps what is already on screen.
1237
+ // Flipping the body to "loading" for a few hundred milliseconds
1238
+ // collapsed the whole workbench and snapped it back — that is the
1239
+ // jump you saw after clicking ↻ or any action button. Only a first
1240
+ // load (or a workspace switch) has nothing to keep.
1241
+ var keep = cwd !== null && cwd === state.cwd && state.phase === "ready";
1242
+ emit(Object.assign({}, state, {
1243
+ sessionId: sessionId,
1244
+ cwd: cwd,
1245
+ phase: keep ? state.phase : (cwd ? "loading" : "idle"),
1246
+ refreshing: keep,
1247
+ error: keep ? state.error : null
1248
+ }));
946
1249
  if (!cwd) return Promise.resolve();
947
1250
  return Promise.all([
948
1251
  verbs.status(cwd),
@@ -1052,7 +1355,7 @@ window.__ModuleLoader__.load({
1052
1355
  };
1053
1356
  var OPTGROUP_STYLE = {
1054
1357
  background: "var(--dsw-alias-bg-base, #1e1e1e)",
1055
- color: "var(--dsw-alias-label-tertiary, #9aa0a6)",
1358
+ color: "var(--dsw-alias-label-secondary, #c8ccd2)",
1056
1359
  fontWeight: 600
1057
1360
  };
1058
1361
 
@@ -1203,6 +1506,32 @@ window.__ModuleLoader__.load({
1203
1506
  var PANEL_MARGIN = 24;
1204
1507
  /** Grab width of the panel's right-edge resize handle. */
1205
1508
  var PANEL_RESIZE_GRIP = 6;
1509
+ /**
1510
+ * Height the panel takes while the diff pane is open: a fixed height, not a
1511
+ * cap. With a cap the panel's height followed its content, so the moment the
1512
+ * diff arrived the pane grew and — anchored by `bottom` — the whole panel
1513
+ * jumped (the flash you saw when opening a diff). The `min()` keeps the panel
1514
+ * on screen in a short window (168px is the panel's bottom offset).
1515
+ */
1516
+ var DIFF_PANEL_HEIGHT = "min(72vh, calc(100vh - 184px))";
1517
+
1518
+ /**
1519
+ * The panel's box in either mode: the compact single column, or workbench +
1520
+ * diff with a height that cannot move once the pane is open.
1521
+ * @param selected - the change the diff pane shows, or null.
1522
+ * @param diffWidth - width of the diff pane.
1523
+ * @returns the panel's inline style.
1524
+ */
1525
+ function panelBoxStyle(selected, diffWidth) {
1526
+ var box = Object.assign({}, S.panel, {
1527
+ width: selected === null ? S.panel.width : DIFF_LEFT_PANE_WIDTH + PANEL_BORDER_WIDTH + diffWidth
1528
+ });
1529
+ if (selected !== null) {
1530
+ box.height = DIFF_PANEL_HEIGHT;
1531
+ box.maxHeight = DIFF_PANEL_HEIGHT;
1532
+ }
1533
+ return box;
1534
+ }
1206
1535
 
1207
1536
  /** The last path segment, for chip-sized text. */
1208
1537
  function baseName(file) {
@@ -1510,10 +1839,14 @@ window.__ModuleLoader__.load({
1510
1839
  // with "selector is not a function".
1511
1840
  var sessions = useSessions(function (s) { return s; });
1512
1841
 
1513
- // NOTE: the shell's seed react-dom is an experimental 18.3.1-next
1514
- // build where React.useSyncExternalStore misbehaves (returns
1515
- // undefined / throws), so subscribe through the classic
1516
- // useState + useEffect pattern instead safe on any React.
1842
+ // NOTE: subscribe through the classic useState + useEffect pattern
1843
+ // instead of React.useSyncExternalStore, so this store's own
1844
+ // subscription is independent of whichever implementation the seed
1845
+ // ships. This is about keeping one dependency out of the path, not a
1846
+ // claim that the seeded build is broken: the framework's own
1847
+ // `useSessions` prop already goes through the renderer's
1848
+ // useSyncExternalStoreWithSelector, so an unusable seed would break
1849
+ // the session list before it broke this panel.
1517
1850
  var stateRef = React.useState(function () { return store.getSnapshot(); });
1518
1851
  var state = stateRef[0];
1519
1852
  var setState = stateRef[1];
@@ -1534,16 +1867,10 @@ window.__ModuleLoader__.load({
1534
1867
  var sessionId = state.sessionId;
1535
1868
  var currentSummary = sessionId !== null && sessions && sessions.byId ? sessions.byId[sessionId] : undefined;
1536
1869
 
1537
- // The session's own model route, when one is recorded: the durable
1538
- // `modelSelection` projection, pending pick first, last used second.
1539
- // Generation falls back to the host's first registered route.
1540
- var sessionRoute = (function () {
1541
- var selection = currentSummary && currentSummary.projectionValues ? currentSummary.projectionValues.modelSelection : undefined;
1542
- if (!selection) return null;
1543
- var effective = selection.next || selection.lastUsed;
1544
- if (!effective || !effective.provider || !effective.model) return null;
1545
- return { provider: effective.provider, model: effective.model };
1546
- })();
1870
+ // The session's own model route, when one is recorded. Both projection
1871
+ // surfaces it can come from are handled by sessionModelRoute; a
1872
+ // missing route leaves generation to the host's first registered one.
1873
+ var sessionRoute = sessionModelRoute(currentSummary, sessions, sessionId);
1547
1874
 
1548
1875
  var messageRef = React.useState("");
1549
1876
  var message = messageRef[0];
@@ -1553,12 +1880,22 @@ window.__ModuleLoader__.load({
1553
1880
  var generateModeRef = React.useState("staged");
1554
1881
  var generateMode = generateModeRef[0];
1555
1882
  var setGenerateMode = generateModeRef[1];
1556
- var changesOpenRef = React.useState(false);
1883
+ // Sections start OPEN: a Git panel that hides the user's own changes
1884
+ // behind a chevron is a panel that looks empty. Collapsing is the
1885
+ // explicit choice, and widening a long list past its preview is a
1886
+ // separate one (`…All`), so the chevron always tells the truth.
1887
+ var changesOpenRef = React.useState(true);
1557
1888
  var changesOpen = changesOpenRef[0];
1558
1889
  var setChangesOpen = changesOpenRef[1];
1559
- var logOpenRef = React.useState(false);
1890
+ var changesAllRef = React.useState(false);
1891
+ var changesAll = changesAllRef[0];
1892
+ var setChangesAll = changesAllRef[1];
1893
+ var logOpenRef = React.useState(true);
1560
1894
  var logOpen = logOpenRef[0];
1561
1895
  var setLogOpen = logOpenRef[1];
1896
+ var logAllRef = React.useState(false);
1897
+ var logAll = logAllRef[0];
1898
+ var setLogAll = logAllRef[1];
1562
1899
  // Whether the last operation's git output is expanded. A new result
1563
1900
  // always starts collapsed: the summary line is the notification, the
1564
1901
  // raw output is opt-in.
@@ -1820,11 +2157,7 @@ window.__ModuleLoader__.load({
1820
2157
  // while nothing is selected, and the two-pane width (workbench + diff)
1821
2158
  // once a file is open. Dragging the panel's right edge is what resizes
1822
2159
  // 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
- });
2160
+ var panelStyle = panelBoxStyle(selected, diffWidth);
1828
2161
  if (dragPos) {
1829
2162
  panelStyle = Object.assign(panelStyle, {
1830
2163
  left: dragPos.left,
@@ -1858,20 +2191,24 @@ window.__ModuleLoader__.load({
1858
2191
  "data-dsh-git": "panel-drag"
1859
2192
  }, [
1860
2193
  h("span", { key: "title", style: S.title }, "⎇ " + t("panel.title")),
1861
- h("span", { key: "cwd", style: S.meta, title: cwd }, shortText(cwd, 60)),
2194
+ h("span", { key: "cwd", style: S.cwdPath, title: cwd }, shortText(cwd, 80)),
1862
2195
  h("button", {
1863
2196
  key: "refresh",
1864
2197
  type: "button",
1865
- style: S.button,
1866
- disabled: busy !== null,
2198
+ // A re-read keeps the whole workbench on screen and reports itself
2199
+ // here instead of collapsing the body to a "loading" line and
2200
+ // snapping back — that flash was the jump.
2201
+ style: Object.assign({}, S.iconButton, busy !== null || state.refreshing ? S.buttonDisabled : null),
2202
+ disabled: busy !== null || state.refreshing === true,
1867
2203
  onClick: function () { store.refresh(state.cwd); },
1868
2204
  "aria-label": t("refresh.aria"),
1869
- title: t("refresh.aria")
1870
- }, ""),
2205
+ "aria-busy": state.refreshing === true ? "true" : "false",
2206
+ title: state.refreshing === true ? t("refresh.busy") : t("refresh.aria")
2207
+ }, state.refreshing === true ? "⋯" : "↻"),
1871
2208
  h("button", {
1872
2209
  key: "collapse",
1873
2210
  type: "button",
1874
- style: S.button,
2211
+ style: S.iconButton,
1875
2212
  onClick: function () { store.setPanelOpen(false); },
1876
2213
  "aria-label": t("panel.collapse"),
1877
2214
  title: t("panel.collapse")
@@ -1896,15 +2233,21 @@ window.__ModuleLoader__.load({
1896
2233
  } else if (!state.repo) {
1897
2234
  bodyChildren.push(h("div", { key: "norepo", style: S.meta }, t("notRepo")));
1898
2235
  } else {
1899
- // status line
2236
+ // status line — the branch itself lives in the select right below,
2237
+ // so this row carries only what the select cannot say: pending
2238
+ // changes and how the branch sits against its upstream.
1900
2239
  var statusLine = [];
1901
2240
  var branchText = state.branch;
1902
2241
  if (!branchText && state.detached) {
1903
2242
  branchText = t("detached") + (state.oid ? " " + state.oid.slice(0, 7) : "");
1904
2243
  }
1905
- statusLine.push(h("span", { key: "chip", style: S.chip, title: branchText || cwd }, branchText || "—"));
2244
+ if (state.detached) {
2245
+ statusLine.push(h("span", { key: "chip", style: S.chip, title: branchText }, branchText));
2246
+ }
1906
2247
  if (state.dirty > 0) {
1907
- statusLine.push(h("span", { key: "dirty", style: S.dirty, title: t("dirty", { count: state.dirty }) }, "● " + state.dirty));
2248
+ statusLine.push(h("span", { key: "dirty", style: S.dirty, title: t("dirty", { count: state.dirty }) }, "● " + t("dirty", { count: state.dirty })));
2249
+ } else {
2250
+ statusLine.push(h("span", { key: "clean", style: S.meta }, "✓ " + t("changes.none")));
1908
2251
  }
1909
2252
  if (state.ahead > 0 || state.behind > 0) {
1910
2253
  statusLine.push(h("span", { key: "ab", style: S.meta }, "↑" + state.ahead + " ↓" + state.behind));
@@ -2024,94 +2367,96 @@ window.__ModuleLoader__.load({
2024
2367
  ]));
2025
2368
  }
2026
2369
 
2027
- // network + commit row
2370
+ // network actions three equal columns. Auto-width labels used to
2371
+ // give each button a different width for the same job, which is what
2372
+ // made this row read as a pile rather than a toolbar.
2028
2373
  bodyChildren.push(h("div", { key: "net", style: S.row }, [
2029
2374
  h("button", {
2030
2375
  key: "fetch",
2031
2376
  type: "button",
2032
- style: Object.assign({}, S.button, busy !== null ? S.buttonDisabled : null),
2377
+ style: Object.assign({}, S.button, S.grow, busy !== null ? S.buttonDisabled : null),
2033
2378
  disabled: busy !== null,
2034
2379
  onClick: function () { store.act("fetch", function () { return store.verbs.fetch(state.cwd); }); }
2035
2380
  }, busy === "fetch" ? t("busy.fetch") : t("action.fetch")),
2036
2381
  h("button", {
2037
2382
  key: "pull",
2038
2383
  type: "button",
2039
- style: Object.assign({}, S.button, busy !== null ? S.buttonDisabled : null),
2384
+ style: Object.assign({}, S.button, S.grow, busy !== null ? S.buttonDisabled : null),
2040
2385
  disabled: busy !== null,
2041
2386
  onClick: function () { store.act("pull", function () { return store.verbs.pull(state.cwd); }); }
2042
2387
  }, busy === "pull" ? t("busy.pull") : t("action.pull")),
2043
2388
  h("button", {
2044
2389
  key: "push",
2045
2390
  type: "button",
2046
- style: Object.assign({}, S.buttonPrimary, busy !== null ? S.buttonDisabled : null),
2391
+ style: Object.assign({}, S.buttonPrimary, S.grow, busy !== null ? S.buttonDisabled : null),
2047
2392
  disabled: busy !== null,
2048
2393
  onClick: function () { store.act("push", function () { return store.verbs.push(state.cwd); }); }
2049
2394
  }, busy === "push" ? t("busy.push") : t("action.push"))
2050
2395
  ]));
2051
2396
 
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.
2397
+ // commit flow, one card: stage all draft basis + draft → message →
2398
+ // commit. Grouping it keeps the 400px column readable and puts the
2399
+ // primary action (提交) in the same surface as the text it sends.
2056
2400
  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"))
2401
+ bodyChildren.push(h("div", { key: "commitCard", style: S.card }, [
2402
+ h("div", { key: "tools", style: S.rowTight }, [
2403
+ h("button", {
2404
+ key: "stage",
2405
+ type: "button",
2406
+ style: Object.assign({}, S.button, S.grow, busy !== null || noChanges ? S.buttonDisabled : null),
2407
+ disabled: busy !== null || noChanges,
2408
+ title: noChanges ? t("changes.none") : t("action.stage"),
2409
+ onClick: function () { store.act("stage", function () { return store.verbs.stage(state.cwd); }); }
2410
+ }, busy === "stage" ? t("busy.stage") : t("action.stage")),
2411
+ h("select", {
2412
+ key: "mode",
2413
+ style: Object.assign({}, S.select, S.grow, { maxWidth: "none" }),
2414
+ value: generateMode,
2415
+ disabled: busy !== null,
2416
+ title: t("generate.mode.title"),
2417
+ "aria-label": t("generate.mode.title"),
2418
+ onChange: function (event) { setGenerateMode(event.target.value); }
2419
+ }, [
2420
+ h("option", { key: "staged", value: "staged", style: OPTION_STYLE }, t("generate.mode.staged")),
2421
+ h("option", { key: "unstaged", value: "unstaged", style: OPTION_STYLE }, t("generate.mode.unstaged")),
2422
+ h("option", { key: "all", value: "all", style: OPTION_STYLE }, t("generate.mode.all"))
2423
+ ]),
2424
+ h("button", {
2425
+ key: "generate",
2426
+ type: "button",
2427
+ style: Object.assign({}, S.button, S.grow, busy !== null || noChanges ? S.buttonDisabled : null),
2428
+ disabled: busy !== null || noChanges,
2429
+ title: noChanges ? t("changes.none") : t("action.generate"),
2430
+ onClick: doGenerate
2431
+ }, busy === "generate" ? t("busy.generate") : "✨ " + t("action.generate"))
2078
2432
  ]),
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();
2433
+ h("div", { key: "message", style: S.rowTight }, [
2434
+ h("textarea", {
2435
+ key: "msg",
2436
+ rows: 2,
2437
+ style: S.textarea,
2438
+ value: message,
2439
+ placeholder: t("commit.placeholder"),
2440
+ disabled: busy !== null,
2441
+ onChange: function (event) { setMessage(event.target.value); },
2442
+ // Enter inserts a newline (a message may have a body); the
2443
+ // explicit commit chord is Ctrl/Cmd+Enter.
2444
+ onKeyDown: function (event) {
2445
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey) && message.trim() !== "" && busy === null) {
2446
+ event.preventDefault();
2447
+ doCommit();
2448
+ }
2104
2449
  }
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"))
2450
+ }),
2451
+ h("button", {
2452
+ key: "commit",
2453
+ type: "button",
2454
+ style: Object.assign({}, S.buttonPrimary, { alignSelf: "flex-end", padding: "0 14px" }, busy !== null || message.trim() === "" ? S.buttonDisabled : null),
2455
+ disabled: busy !== null || message.trim() === "",
2456
+ title: message.trim() === "" ? t("commit.empty") : undefined,
2457
+ onClick: doCommit
2458
+ }, busy === "commit" ? t("busy.commit") : t("action.commit"))
2459
+ ])
2115
2460
  ]));
2116
2461
 
2117
2462
  function doCommit() {
@@ -2141,7 +2486,26 @@ window.__ModuleLoader__.load({
2141
2486
  };
2142
2487
  var text = known[details.code];
2143
2488
  if (text === undefined) {
2144
- text = res && res.error && res.error.message ? String(res.error.message) : t("generate.failed");
2489
+ // Unknown code (a transport failure, a provider rejection,
2490
+ // …): lead with the localized sentence, keep the host's
2491
+ // own words as the diagnosis, and name the route that
2492
+ // failed — "which provider is broken" is the actionable
2493
+ // part when a gateway refuses the call.
2494
+ var detail = res && res.error && res.error.message ? String(res.error.message) : "";
2495
+ var failedRoute = details.provider && details.model
2496
+ ? { provider: details.provider, model: details.model }
2497
+ : null;
2498
+ if (detail === "") {
2499
+ text = t("generate.failed");
2500
+ } else if (failedRoute !== null) {
2501
+ text = t("generate.failedRoute", {
2502
+ message: shortText(detail, 160),
2503
+ provider: failedRoute.provider,
2504
+ model: failedRoute.model
2505
+ });
2506
+ } else {
2507
+ text = t("generate.failedDetail", { message: shortText(detail, 160) });
2508
+ }
2145
2509
  }
2146
2510
  return { kind: "error", text: text };
2147
2511
  }
@@ -2154,7 +2518,8 @@ window.__ModuleLoader__.load({
2154
2518
  state.cwd,
2155
2519
  generateMode,
2156
2520
  route ? route.provider : undefined,
2157
- route ? route.model : undefined
2521
+ route ? route.model : undefined,
2522
+ state.sessionId === null ? undefined : state.sessionId
2158
2523
  );
2159
2524
  }, describeGenerate).then(function (res) {
2160
2525
  if (res && res.ok === true && res.value && res.value.message) {
@@ -2163,79 +2528,67 @@ window.__ModuleLoader__.load({
2163
2528
  });
2164
2529
  }
2165
2530
 
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
- }
2531
+ // changes (one row per file) — the diff viewer's navigation:
2532
+ // clicking a row opens that file's diff on the right. The chip
2533
+ // trails the path so paths line up in one column and the eye can
2534
+ // scan them; the status only has to say which kind of change it is.
2535
+ var changesRows = state.changes.map(function (change, index) {
2536
+ var file = change.file !== undefined ? change.file : change.path;
2537
+ var origFile = change.origFile !== undefined ? change.origFile : null;
2538
+ var isActive = selected !== null && selected.file === file && selected.origFile === origFile;
2539
+ return h("div", {
2540
+ key: index,
2541
+ style: isActive ? S.changeRowActive : S.changeRow,
2542
+ role: "button",
2543
+ tabIndex: 0,
2544
+ title: change.path,
2545
+ "aria-label": t("diff.open") + " " + change.path,
2546
+ "data-dsh-git": "change-row",
2547
+ "data-active": isActive === true ? "true" : "false",
2548
+ onClick: function () { selectChange(change); },
2549
+ onKeyDown: function (event) {
2550
+ if (event.key === "Enter" || event.key === " ") {
2551
+ if (event.preventDefault) event.preventDefault();
2552
+ selectChange(change);
2192
2553
  }
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));
2554
+ }
2555
+ }, [
2556
+ h("span", { key: "path", style: S.changePath }, change.path),
2557
+ h("span", { key: "tag", style: changeTagStyle(change.status) }, t("status." + change.status))
2558
+ ]);
2559
+ });
2560
+ bodyChildren.push(sectionView({
2561
+ hook: "changes",
2562
+ label: t("changes.title"),
2563
+ count: state.changes.length,
2564
+ open: changesOpen,
2565
+ all: changesAll,
2566
+ cap: CHANGES_PREVIEW,
2567
+ empty: " " + t("changes.none"),
2568
+ rows: changesRows,
2569
+ onToggle: function () { setChangesOpen(!changesOpen); },
2570
+ onMore: function () { setChangesAll(!changesAll); }
2571
+ }, t));
2572
+
2573
+ // recent commits — same card, one row per commit
2574
+ var logRows = state.commits.map(function (commit, index) {
2575
+ return h("div", { key: index, style: S.logRow, title: commit.subject }, [
2576
+ h("span", { key: "sha", style: S.logSha }, commit.sha),
2577
+ h("span", { key: "subj", style: S.logSubject }, commit.subject)
2578
+ ]);
2579
+ });
2580
+ bodyChildren.push(sectionView({
2581
+ hook: "log",
2582
+ label: t("log.title"),
2583
+ count: state.commits.length,
2584
+ open: logOpen,
2585
+ all: logAll,
2586
+ cap: LOG_PREVIEW,
2587
+ empty: t("log.none"),
2588
+ rows: logRows,
2589
+ onToggle: function () { setLogOpen(!logOpen); },
2590
+ onMore: function () { setLogAll(!logAll); }
2591
+ }, t));
2239
2592
  }
2240
2593
 
2241
2594
  // Everything the open diff depends on, as one comparable string: a
@@ -2345,7 +2698,8 @@ window.__ModuleLoader__.load({
2345
2698
  ? true
2346
2699
  : (summary.retainedBy.mainView || 0) > 0;
2347
2700
 
2348
- // Classic subscription pattern (see GitFloatingPanel for why).
2701
+ // Classic subscription pattern (see GitFloatingPanel's note: keep this
2702
+ // store independent of the seed's useSyncExternalStore build).
2349
2703
  var stateRef = React.useState(function () { return store.getSnapshot(); });
2350
2704
  var state = stateRef[0];
2351
2705
  var setState = stateRef[1];
@@ -2381,17 +2735,22 @@ window.__ModuleLoader__.load({
2381
2735
  }
2382
2736
 
2383
2737
  // Left-aligned compact pill in the row band ABOVE the composer card
2384
- // (conversation.input.dock). The padding-left mirrors the centered
2385
- // card's left margin, so the pill hugs the textarea's top-left
2386
- // corner instead of floating at the conversation column's edge.
2738
+ // (conversation.input.dock). The row box is capped at the composer
2739
+ // card's own width and centered — the same way ui-conversation's own
2740
+ // dock occupant (QueueDock) aligns itself — so the pill hugs the
2741
+ // textarea's top-left corner instead of floating at the conversation
2742
+ // column's edge. `--dsh-composer-card-max-width` is ui-conversation's
2743
+ // internal variable (no published contract, and 0.1.7 added a second
2744
+ // value for the embedded body); the 778px fallback keeps this row sane
2745
+ // if it is ever renamed.
2387
2746
  return h("div", {
2388
2747
  style: {
2389
2748
  boxSizing: "border-box",
2390
2749
  display: "flex",
2391
2750
  justifyContent: "flex-start",
2392
2751
  width: "100%",
2393
- paddingLeft: "calc((100% - var(--dsh-composer-card-max-width, 778px)) / 2)",
2394
- margin: "2px 0"
2752
+ maxWidth: "var(--dsh-composer-card-max-width, 778px)",
2753
+ margin: "2px auto"
2395
2754
  },
2396
2755
  "data-dsh-git": "dock-row"
2397
2756
  }, h("button", {
@@ -2403,7 +2762,7 @@ window.__ModuleLoader__.load({
2403
2762
  maxWidth: 220,
2404
2763
  padding: "1px 10px",
2405
2764
  borderRadius: 999,
2406
- border: "1px solid var(--dsw-alias-separator-primary, rgba(127,127,127,0.25))",
2765
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.25))",
2407
2766
  background: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.12))",
2408
2767
  color: "var(--dsw-alias-label-secondary, #c8ccd2)",
2409
2768
  font: "inherit",
@@ -2472,7 +2831,12 @@ window.__ModuleLoader__.load({
2472
2831
  GitDiffPane: GitDiffPane,
2473
2832
  DIFF_ROW: DIFF_ROW,
2474
2833
  stripAnsiEscapes: stripAnsiEscapes,
2475
- outputView: outputView
2834
+ outputView: outputView,
2835
+ sectionView: sectionView,
2836
+ changeTagStyle: changeTagStyle,
2837
+ panelBoxStyle: panelBoxStyle,
2838
+ DIFF_PANEL_HEIGHT: DIFF_PANEL_HEIGHT,
2839
+ sessionModelRoute: sessionModelRoute
2476
2840
  };
2477
2841
  return module.exports;
2478
2842
  }