newmark-agent 0.5.4 → 0.5.7

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.
@@ -80,6 +80,7 @@ try {
80
80
  --accent-glow: rgba(91,120,255,0.3);
81
81
  --accent2: #38d4a0;
82
82
  --accent2-glow: rgba(56,212,160,0.3);
83
+ --chat-canvas: color-mix(in srgb, var(--app-bg) 92%, var(--accent) 8%);
83
84
 
84
85
  /* Glass layers */
85
86
  --glass-alpha: 0.85;
@@ -97,6 +98,16 @@ try {
97
98
  --glass-border-2: rgba(255,255,255,0.08);
98
99
  --glass-border-3: rgba(255,255,255,0.10);
99
100
 
101
+ /* Kyant AndroidLiquidGlass (backdrop) liquid-glass parity tokens.
102
+ CSS equivalent of the library lens edge refraction, top highlight,
103
+ inner shadow and saturation boost (saturate(140%) == vibrancy). */
104
+ --liquid-edge-glow: rgba(255,255,255,0.10);
105
+ --liquid-edge-glow-strong: rgba(255,255,255,0.16);
106
+ --liquid-top-highlight: inset 0 1px 0 rgba(255,255,255,0.10);
107
+ --liquid-inner-shadow: inset 0 -1px 0 rgba(0,0,0,0.22);
108
+ --liquid-outer-shadow: 0 8px 32px rgba(0,0,0,0.45);
109
+ --liquid-refraction: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0) 18%, rgba(0,0,0,0) 82%, rgba(0,0,0,0.18));
110
+
100
111
  /* Borders & corners */
101
112
  --border: rgba(255,255,255,0.06);
102
113
  --border-hover: rgba(255,255,255,0.12);
@@ -130,6 +141,15 @@ try {
130
141
  --glass-blur-1: 1.2px;
131
142
  --glass-blur-2: 2.4px;
132
143
  --glass-blur-3: 3px;
144
+ /* One physical glass setting, two material roles. Carrier glass frosts the
145
+ backdrop; the moving float stays clear and only refracts/ disperses it. */
146
+ --carrier-glass-blur: 6.6px;
147
+ --carrier-glass-alpha: 0.374;
148
+ --liquid-interaction-edge: 6px;
149
+ --liquid-float-refraction-height: var(--liquid-interaction-edge);
150
+ --liquid-float-refraction-amount: 24px;
151
+ --liquid-float-chromatic-aberration: 1;
152
+ --liquid-float-optical-opacity: 1;
133
153
 
134
154
  /* Transitions */
135
155
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
@@ -211,8 +231,9 @@ try {
211
231
  --glass-rgb-1: 255 255 255;
212
232
  --glass-rgb-2: 255 255 255;
213
233
  --glass-rgb-3: 255 255 255;
214
- --modal-surface: rgba(248,250,255,0.76);
215
- --modal-shadow: 0 24px 80px rgba(41,55,92,0.22), 0 0 42px rgba(255,255,255,0.72), inset 0 1px 0 rgba(255,255,255,0.96), inset 0 0 28px rgba(255,255,255,0.42);
234
+ --modal-surface: rgba(248,250,255,0.82);
235
+ --settings-modal-surface: rgba(255,255,255,0.94);
236
+ --modal-shadow: 0 24px 70px rgba(41,55,92,0.20), inset 0 1px 0 rgba(255,255,255,0.62), inset 0 -1px 0 rgba(46,58,92,0.10);
216
237
  --glass-border-1: rgba(0,0,0,0.06);
217
238
  --glass-border-2: rgba(0,0,0,0.08);
218
239
  --glass-border-3: rgba(0,0,0,0.10);
@@ -223,6 +244,7 @@ try {
223
244
  --shadow-md: 0 2px 8px rgba(40,52,86,0.07);
224
245
  --shadow-lg: 0 6px 18px rgba(40,52,86,0.09);
225
246
  --accent-glow: rgba(74,104,223,0.25);
247
+ --chat-canvas: color-mix(in srgb, var(--app-bg) 96%, var(--accent) 4%);
226
248
  }
227
249
 
228
250
  /* --- Reset & Base --- */
@@ -357,6 +379,9 @@ html, body {
357
379
  border: 0;
358
380
  }
359
381
  button:focus, select:focus, input:focus, textarea:focus, [contenteditable]:focus { outline: none; }
382
+ /* Liquid-glass controls use one continuous capsule edge. Equal-width icon
383
+ buttons naturally become circles; wider controls become end-rounded pills. */
384
+ button { border-radius: var(--radius-full) !important; }
360
385
  :where(button, select, input, textarea, [contenteditable], [tabindex]):focus-visible {
361
386
  outline: 2px solid var(--accent);
362
387
  outline-offset: 2px;
@@ -505,6 +530,12 @@ button:focus, select:focus, input:focus, textarea:focus, [contenteditable]:focus
505
530
  transition: opacity var(--duration-normal) var(--ease-out-expo), transform var(--duration-normal) var(--ease-out-expo), visibility 0ms linear var(--duration-normal);
506
531
  }
507
532
 
533
+ #left-tool-surface {
534
+ /* Gesture delegation only: do not introduce another flex item. Its children
535
+ retain the original direct-child layout inside #left-content. */
536
+ display: contents;
537
+ }
538
+
508
539
  #left-thumb {
509
540
  display: flex;
510
541
  position: absolute;
@@ -630,7 +661,7 @@ button.left-nav-icon { width: calc(100% - 12px); }
630
661
  transition: max-width var(--duration-slow) var(--ease-out-expo) 80ms, opacity var(--duration-normal) var(--ease-out-expo) 40ms, transform var(--duration-slow) var(--ease-out-expo) 80ms, visibility 0ms linear 200ms;
631
662
  }
632
663
 
633
- #left.collapsing #left-content > div[style*="uppercase"],
664
+ #left.collapsing #left-tool-surface > div[style*="uppercase"],
634
665
  #left.collapsing #left-ws-header,
635
666
  #left.collapsing #left-ws-add {
636
667
  opacity: 0;
@@ -676,7 +707,7 @@ button.left-nav-icon { width: calc(100% - 12px); }
676
707
  vertical-align: top;
677
708
  }
678
709
 
679
- #left.collapsed #left-content > div[style*="uppercase"] {
710
+ #left.collapsed #left-tool-surface > div[style*="uppercase"] {
680
711
  height: 0;
681
712
  padding: 0;
682
713
  overflow: hidden;
@@ -774,6 +805,7 @@ button.left-nav-icon { width: calc(100% - 12px); }
774
805
  transition: background var(--duration-fast) var(--ease-out-expo), width var(--duration-normal) var(--ease-out-expo), height var(--duration-normal) var(--ease-out-expo), padding var(--duration-normal) var(--ease-out-expo), margin var(--duration-normal) var(--ease-out-expo), gap var(--duration-normal) var(--ease-out-expo);
775
806
  white-space: nowrap;
776
807
  overflow: hidden;
808
+ border-radius: var(--radius-full);
777
809
  }
778
810
 
779
811
  button.left-ws-item {
@@ -789,7 +821,7 @@ button.left-ws-item {
789
821
 
790
822
  .left-ws-item .ws-icon {
791
823
  width: 20px; height: 20px;
792
- border-radius: var(--radius-sm);
824
+ border-radius: 50%;
793
825
  background: linear-gradient(135deg, var(--accent), #7b93ff);
794
826
  color: #fff;
795
827
  font-size: 10px;
@@ -932,8 +964,8 @@ button.left-ws-item {
932
964
  width: 0;
933
965
  flex-shrink: 0;
934
966
  background: var(--glass-bg-3);
935
- backdrop-filter: blur(var(--glass-blur-3)) saturate(130%);
936
- -webkit-backdrop-filter: blur(var(--glass-blur-3)) saturate(130%);
967
+ backdrop-filter: blur(var(--glass-blur-1));
968
+ -webkit-backdrop-filter: blur(var(--glass-blur-1));
937
969
  border-right: 1px solid var(--glass-border-1);
938
970
  display: flex;
939
971
  flex-direction: column;
@@ -991,6 +1023,7 @@ button.left-ws-item {
991
1023
  flex: 1;
992
1024
  overflow-y: auto;
993
1025
  padding: 4px 8px;
1026
+ position: relative;
994
1027
  }
995
1028
 
996
1029
  .conv-item {
@@ -999,7 +1032,7 @@ button.left-ws-item {
999
1032
  gap: 6px;
1000
1033
  padding: 8px 10px;
1001
1034
  margin: 2px 0;
1002
- border-radius: var(--radius-md);
1035
+ border-radius: var(--radius-full);
1003
1036
  cursor: pointer;
1004
1037
  background: var(--glass-bg-1);
1005
1038
  border: 1px solid var(--glass-border-1);
@@ -1009,7 +1042,7 @@ button.left-ws-item {
1009
1042
  .conv-item:hover { background: var(--glass-bg-2); border-color: var(--border-hover); }
1010
1043
  .conv-item.active { border-color: var(--accent); background: rgba(91,120,255,0.1); }
1011
1044
  .conv-item.marquee-border::before {
1012
- border-radius: var(--radius-md);
1045
+ border-radius: var(--radius-full);
1013
1046
  }
1014
1047
 
1015
1048
  .conv-summary {
@@ -1053,8 +1086,52 @@ button.left-ws-item {
1053
1086
  to { transform: rotate(360deg); }
1054
1087
  }
1055
1088
 
1056
- .conv-item.dragging { opacity: 0.58; }
1057
- .conv-item.drag-over { border-color: var(--accent); background: rgba(91,120,255,0.14); }
1089
+ .conv-item.dragging { opacity: 0; }
1090
+ #conversation-list.conversation-reordering .conv-item.active:not(.conversation-glass-dragging) {
1091
+ border-color: var(--glass-border-1);
1092
+ background: var(--glass-bg-1);
1093
+ }
1094
+ .conv-item.conversation-glass-dragging {
1095
+ background: transparent;
1096
+ background-color: transparent !important;
1097
+ background-image: none !important;
1098
+ backdrop-filter: none;
1099
+ -webkit-backdrop-filter: none;
1100
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 8px 22px rgba(0,0,0,.3);
1101
+ transform: scale(1.06);
1102
+ }
1103
+ .conversation-selection-float.conversation-drag-following {
1104
+ transition: opacity 120ms var(--ease-out-expo) !important;
1105
+ }
1106
+ .conversation-drag-content {
1107
+ position: absolute;
1108
+ inset: 9px;
1109
+ z-index: 2;
1110
+ display: flex;
1111
+ align-items: center;
1112
+ gap: 6px;
1113
+ min-width: 0;
1114
+ padding: 8px 10px;
1115
+ box-sizing: border-box;
1116
+ border: 0;
1117
+ border-radius: var(--radius-full);
1118
+ background: transparent;
1119
+ color: var(--text);
1120
+ pointer-events: none;
1121
+ transform: scale(1);
1122
+ transform-origin: center;
1123
+ }
1124
+ .conversation-drag-content .conv-summary { min-width: 0; flex: 1; }
1125
+ .conversation-drag-content button { pointer-events: none; }
1126
+ .conversation-selection-float.liquid-selection-landing > .conversation-drag-content {
1127
+ /* The optical shell contracts into the destination color block. Keep the
1128
+ carried label/icons at their original size until ownership returns. */
1129
+ transform: scale(calc(1 / .86));
1130
+ transition: transform 120ms var(--ease-out-expo);
1131
+ }
1132
+ .conversation-selection-float.liquid-selection-landing > .conversation-drag-content {
1133
+ transition-duration: 180ms;
1134
+ }
1058
1135
  .conv-rename-input {
1059
1136
  flex: 1;
1060
1137
  min-width: 0;
@@ -1124,10 +1201,9 @@ button.left-ws-item {
1124
1201
  overflow: auto;
1125
1202
  padding: 6px;
1126
1203
  border: 1px solid var(--glass-border-2);
1127
- border-radius: var(--radius-lg);
1204
+ border-radius: var(--radius-xl);
1128
1205
  background: var(--modal-surface);
1129
- box-shadow: var(--shadow-lg);
1130
- backdrop-filter: blur(var(--glass-blur-3)) saturate(1.08);
1206
+ box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
1131
1207
  }
1132
1208
  .conv-action-menu:popover-open { display: block; animation: model-menu-in 150ms var(--ease-out-expo); }
1133
1209
 
@@ -1227,6 +1303,7 @@ button.left-ws-item {
1227
1303
  backdrop-filter: blur(var(--glass-blur-2)) saturate(130%);
1228
1304
  -webkit-backdrop-filter: blur(var(--glass-blur-2)) saturate(130%);
1229
1305
  border-left: 1px solid var(--glass-border-1);
1306
+ border-radius: var(--radius-lg) 0 0 var(--radius-lg);
1230
1307
  display: flex;
1231
1308
  flex-direction: column;
1232
1309
  overflow: hidden;
@@ -1236,7 +1313,7 @@ button.left-ws-item {
1236
1313
  #right.open {
1237
1314
  width: var(--right-width);
1238
1315
  opacity: 1;
1239
- box-shadow: -18px 0 40px rgba(0,0,0,0.16);
1316
+ box-shadow: none;
1240
1317
  }
1241
1318
 
1242
1319
  [data-theme="light"] #right.open {
@@ -1792,8 +1869,6 @@ button.left-ws-item {
1792
1869
  height: 34px;
1793
1870
  flex-shrink: 0;
1794
1871
  background: var(--glass-bg-3);
1795
- backdrop-filter: blur(var(--glass-blur-3)) saturate(130%);
1796
- -webkit-backdrop-filter: blur(var(--glass-blur-3)) saturate(130%);
1797
1872
  border-top: 1px solid var(--glass-border-1);
1798
1873
  display: flex;
1799
1874
  flex-direction: column;
@@ -2057,6 +2132,8 @@ button.left-ws-item {
2057
2132
  overflow: hidden;
2058
2133
  position: relative;
2059
2134
  min-height: 0;
2135
+ /* Opaque theme-tinted foundation below the transcript, rails and glass. */
2136
+ background-color: var(--chat-canvas);
2060
2137
  }
2061
2138
 
2062
2139
  #chat-area {
@@ -3296,8 +3373,7 @@ button.todo-item { background: transparent; }
3296
3373
  padding: 12px 16px;
3297
3374
  border-top: 1px solid var(--glass-border-2);
3298
3375
  background: var(--glass-bg-2);
3299
- backdrop-filter: blur(var(--glass-blur-3)) saturate(140%);
3300
- -webkit-backdrop-filter: blur(var(--glass-blur-3)) saturate(140%);
3376
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow);
3301
3377
  }
3302
3378
 
3303
3379
  #input-area textarea {
@@ -3443,10 +3519,9 @@ button.todo-item { background: transparent; }
3443
3519
  overflow: auto;
3444
3520
  padding: 7px;
3445
3521
  border: 1px solid var(--glass-border-2);
3446
- border-radius: var(--radius-lg);
3522
+ border-radius: var(--radius-xl);
3447
3523
  background: var(--modal-surface);
3448
- box-shadow: var(--shadow-lg);
3449
- backdrop-filter: blur(var(--glass-blur-3)) saturate(1.08);
3524
+ box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
3450
3525
  }
3451
3526
  .model-select-menu:popover-open { display: block; animation: model-menu-in 150ms var(--ease-out-expo); }
3452
3527
 
@@ -3470,6 +3545,85 @@ button.todo-item { background: transparent; }
3470
3545
  .model-select-menu-option:hover,
3471
3546
  .model-select-menu-option:focus-visible { background: var(--control-hover-bg); color: var(--text-bright); outline: none; }
3472
3547
  .model-select-menu-option.selected { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--text-bright); }
3548
+ .liquid-selection-source { background: transparent !important; border-color: transparent !important; }
3549
+ .liquid-selection-source::after { display: none !important; opacity: 0 !important; }
3550
+ .conv-item.liquid-selection-covered { border-color: transparent !important; }
3551
+ .conv-item.liquid-selection-source.marquee-border::before,
3552
+ .conv-item.liquid-selection-covered.marquee-border::before { opacity: 0 !important; }
3553
+ /* Rails and selectable menus are governed by one shared moving glass block.
3554
+ Suppress the generic pressed pseudo-glass so the destination never creates
3555
+ a second block while the source block is flying toward it. */
3556
+ .model-select-menu-option::after,
3557
+ .newmark-select-option::after,
3558
+ .left-nav-icon::after,
3559
+ .lt-item::after,
3560
+ .left-ws-item::after,
3561
+ .tab-btn::after,
3562
+ .stab-btn[data-stab]::after,
3563
+ .stab-btn[data-plugin-tab]::after,
3564
+ .mode-toggle-btn[data-mode]::after,
3565
+ .memory-lab-view-menu button[data-memory-view]::after {
3566
+ display: none !important;
3567
+ opacity: 0 !important;
3568
+ }
3569
+ .left-ws-item.liquid-selection-source,
3570
+ .tab-btn.liquid-selection-source { background: transparent !important; }
3571
+ #left-ws-list,
3572
+ #right-tabs { position: relative; }
3573
+ .left-nav-icon.liquid-tool-active,
3574
+ .lt-item.liquid-tool-active { background: var(--control-hover-bg); color: var(--text-bright); }
3575
+ .liquid-selection-float {
3576
+ --liquid-lift-scale: .94;
3577
+ --liquid-motion-angle: 0rad;
3578
+ --liquid-motion-stretch: 1;
3579
+ --liquid-motion-squash: 1;
3580
+ position: absolute;
3581
+ z-index: 4;
3582
+ display: flex;
3583
+ align-items: center;
3584
+ padding: 6px 10px;
3585
+ box-sizing: border-box;
3586
+ border-radius: var(--radius-full);
3587
+ color: var(--text-bright);
3588
+ pointer-events: none;
3589
+ overflow: visible;
3590
+ opacity: 0;
3591
+ transform: scale(var(--liquid-lift-scale)) rotate(var(--liquid-motion-angle)) scaleX(var(--liquid-motion-stretch)) scaleY(var(--liquid-motion-squash)) rotate(calc(-1 * var(--liquid-motion-angle)));
3592
+ transform-origin: center;
3593
+ background: transparent;
3594
+ background-color: transparent !important;
3595
+ background-image: none !important;
3596
+ backdrop-filter: none;
3597
+ -webkit-backdrop-filter: none;
3598
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), inset 0 -1px 0 rgba(0,0,0,0.18), 0 9px 24px rgba(0,0,0,0.28);
3599
+ transition: top 240ms var(--ease-out-expo), left 240ms var(--ease-out-expo), width 240ms var(--ease-out-expo), height 240ms var(--ease-out-expo), opacity 80ms var(--ease-out-expo), transform 24ms linear;
3600
+ will-change: top, left, width, height, transform;
3601
+ }
3602
+ .liquid-selection-float.visible { opacity: 1; --liquid-lift-scale: 1; }
3603
+ .liquid-selection-float.liquid-selection-landing {
3604
+ opacity: 0;
3605
+ --liquid-lift-scale: .86;
3606
+ transform: scale(var(--liquid-lift-scale)) rotate(var(--liquid-motion-angle)) scaleX(var(--liquid-motion-stretch)) scaleY(var(--liquid-motion-squash)) rotate(calc(-1 * var(--liquid-motion-angle)));
3607
+ transition: opacity 120ms var(--ease-out-expo), transform 120ms var(--ease-out-expo) !important;
3608
+ }
3609
+ .liquid-selection-float.liquid-selection-viewport {
3610
+ position: fixed;
3611
+ inset: auto;
3612
+ z-index: 1000;
3613
+ margin: 0;
3614
+ border: 0;
3615
+ }
3616
+ .liquid-selection-float.liquid-selection-primed { transition: none !important; }
3617
+ .liquid-selection-float.liquid-selection-viewport:popover-open { display: flex; }
3618
+ .liquid-selection-float::-webkit-scrollbar { display: none; }
3619
+ .liquid-selection-canvas {
3620
+ position: absolute;
3621
+ inset: 0;
3622
+ width: 100%;
3623
+ height: 100%;
3624
+ border-radius: inherit;
3625
+ pointer-events: none;
3626
+ }
3473
3627
  @keyframes model-menu-in { from { opacity: 0; transform: translateY(5px) scale(.985); } to { opacity: 1; transform: none; } }
3474
3628
 
3475
3629
  .newmark-select-shell {
@@ -3508,8 +3662,7 @@ button.todo-item { background: transparent; }
3508
3662
  border: 1px solid var(--glass-border-2);
3509
3663
  border-radius: var(--radius-lg);
3510
3664
  background: var(--modal-surface);
3511
- box-shadow: var(--shadow-lg);
3512
- backdrop-filter: blur(var(--glass-blur-3)) saturate(1.08);
3665
+ box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
3513
3666
  }
3514
3667
  .newmark-select-shell.open .newmark-select-menu { display: block; animation: model-menu-in 150ms var(--ease-out-expo); }
3515
3668
  .newmark-select-menu-group { padding: 7px 9px 4px; color: var(--text-accent); font: 700 11px var(--font-ui); }
@@ -3530,6 +3683,7 @@ button.todo-item { background: transparent; }
3530
3683
  .newmark-select-option:hover,
3531
3684
  .newmark-select-option:focus-visible { background: var(--control-hover-bg); color: var(--text-bright); outline: none; }
3532
3685
  .newmark-select-option.selected { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--text-bright); }
3686
+ .newmark-select-option.liquid-selection-source { background: transparent !important; }
3533
3687
  .newmark-select-option:disabled { opacity: .42; cursor: not-allowed; }
3534
3688
  .setting-control > .newmark-select-shell,
3535
3689
  .auto-input-group > .newmark-select-shell,
@@ -3668,9 +3822,11 @@ button.todo-item { background: transparent; }
3668
3822
  .mode-toggle {
3669
3823
  display: flex;
3670
3824
  border: 1px solid var(--border);
3671
- border-radius: var(--radius-md);
3672
- overflow: hidden;
3825
+ border-radius: var(--radius-full);
3826
+ overflow: visible;
3673
3827
  flex-shrink: 0;
3828
+ position: relative;
3829
+ padding: 2px;
3674
3830
  }
3675
3831
 
3676
3832
  .mode-toggle-btn {
@@ -3686,9 +3842,25 @@ button.todo-item { background: transparent; }
3686
3842
  }
3687
3843
 
3688
3844
  .mode-toggle-btn:hover { background: rgba(255,255,255,0.05); color: var(--text); }
3689
- .mode-toggle-btn.active { background: rgba(91,120,255,0.15); color: var(--accent); }
3845
+ .mode-toggle-btn.active { background: rgba(91,120,255,0.15); color: var(--accent); border-radius: var(--radius-full); }
3690
3846
  .mode-toggle-btn:active { transform: scale(0.95) translateZ(0); }
3691
3847
 
3848
+ .workspace-type-pager {
3849
+ display: grid;
3850
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3851
+ position: relative;
3852
+ gap: 2px;
3853
+ padding: 3px;
3854
+ margin-bottom: 12px;
3855
+ border: 1px solid var(--border);
3856
+ border-radius: var(--radius-full);
3857
+ overflow: visible;
3858
+ background: var(--control-bg);
3859
+ }
3860
+ .workspace-type-option { display:flex; align-items:center; justify-content:center; min-width:0; padding:7px 10px; border-radius:var(--radius-full); color:var(--text-dim); font-size:12px; cursor:pointer; user-select:none; }
3861
+ .workspace-type-option.active { background:var(--control-hover-bg); color:var(--text-bright); }
3862
+ .workspace-type-option input { position:absolute; opacity:0; pointer-events:none; }
3863
+
3692
3864
  #submit-btn {
3693
3865
  width: 34px; height: 34px;
3694
3866
  border-radius: 50%;
@@ -3809,12 +3981,12 @@ button.todo-item { background: transparent; }
3809
3981
  linear-gradient(#fff 0 0) content-box,
3810
3982
  linear-gradient(#fff 0 0);
3811
3983
  mask-composite: exclude;
3812
- /* A masked conic-gradient whose custom angle changes every frame forces a
3813
- full GPU repaint. Several simultaneous Agent/SubAgent surfaces made that
3814
- repaint saturate the renderer and GPU, starving ordinary text input. Keep
3815
- the shared status border visually present, but static; live progress is
3816
- already communicated by status text, duration, and event updates. */
3817
- animation: none;
3984
+ /* Live marquee rotation. The @property-registered --marquee-angle animates
3985
+ the conic-gradient sweep; Chromium promotes the masked layer so the border
3986
+ stays composited. Release smoke tests verify the animation is active while
3987
+ ordinary text input remains responsive (see release-ui-render-performance
3988
+ and release-ui-icon smoke). */
3989
+ animation: marquee-rotate var(--marquee-speed) linear infinite;
3818
3990
  z-index: -1;
3819
3991
  pointer-events: none;
3820
3992
  transform: translateZ(0);
@@ -3825,7 +3997,7 @@ button.todo-item { background: transparent; }
3825
3997
  background: linear-gradient(var(--marquee-angle, 0deg), var(--g1), var(--g2), var(--g3), var(--g4), var(--g1));
3826
3998
  }
3827
3999
  @keyframes marquee-rotate-fallback { 0% { filter: hue-rotate(0deg); } 100% { filter: hue-rotate(360deg); } }
3828
- .marquee-border::before { animation: none; }
4000
+ .marquee-border::before { animation: marquee-rotate-fallback var(--marquee-speed) linear infinite; }
3829
4001
  }
3830
4002
 
3831
4003
  .working-glow {
@@ -4071,16 +4243,24 @@ button.todo-item { background: transparent; }
4071
4243
  ============================================================ */
4072
4244
  .settings-tabs {
4073
4245
  display: flex;
4246
+ position: relative;
4074
4247
  gap: 4px;
4075
- padding-bottom: 14px;
4076
- border-bottom: 1px solid var(--glass-border-1);
4248
+ width: fit-content;
4249
+ max-width: 100%;
4250
+ padding: 4px;
4251
+ border: 1px solid var(--glass-border-1);
4252
+ border-radius: var(--radius-full);
4253
+ background: var(--glass-bg-1);
4077
4254
  margin-bottom: 14px;
4255
+ overflow-x: auto;
4256
+ scrollbar-width: none;
4078
4257
  }
4258
+ .settings-tabs::-webkit-scrollbar { display: none; }
4079
4259
 
4080
4260
  .stab-btn {
4081
4261
  padding: 6px 14px;
4082
4262
  border: 1px solid var(--border);
4083
- border-radius: var(--radius-md);
4263
+ border-radius: var(--radius-full);
4084
4264
  background: transparent;
4085
4265
  color: var(--text-dim);
4086
4266
  font-size: 12px;
@@ -4391,6 +4571,42 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4391
4571
  box-shadow: 0 0 4px rgba(0,0,0,0.3);
4392
4572
  }
4393
4573
 
4574
+ #settings-glass-opacity {
4575
+ --liquid-range-progress: 85%;
4576
+ height: 6px;
4577
+ border-radius: var(--radius-full);
4578
+ background: linear-gradient(90deg, var(--accent) 0 var(--liquid-range-progress), var(--glass-border-2) var(--liquid-range-progress) 100%);
4579
+ cursor: pointer;
4580
+ }
4581
+ #settings-glass-opacity::-webkit-slider-thumb {
4582
+ width: 16px;
4583
+ height: 16px;
4584
+ border: 0;
4585
+ background: var(--accent);
4586
+ box-shadow: 0 2px 6px rgba(0,0,0,.3);
4587
+ transition: width 120ms var(--ease-out-expo), height 120ms var(--ease-out-expo), transform 120ms var(--ease-out-expo), box-shadow 120ms var(--ease-out-expo);
4588
+ }
4589
+ #settings-glass-opacity.liquid-range-dragging::-webkit-slider-thumb {
4590
+ width: 26px;
4591
+ height: 20px;
4592
+ border-radius: var(--radius-full);
4593
+ background: var(--liquid-refraction);
4594
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
4595
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
4596
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 8px 20px rgba(0,0,0,.34);
4597
+ transform: translateY(-2px) scale(1.22);
4598
+ transition: none;
4599
+ }
4600
+ .settings-glass-range-line { display:flex; align-items:center; gap:10px; }
4601
+ .settings-glass-range-line #settings-glass-opacity { flex:1; min-width:0; }
4602
+ #settings-glass-value {
4603
+ min-width: 42px;
4604
+ color: var(--text-bright);
4605
+ font: 650 11px var(--font-mono);
4606
+ text-align: right;
4607
+ font-variant-numeric: tabular-nums;
4608
+ }
4609
+
4394
4610
  .setting-desc {
4395
4611
  font-size: 10px;
4396
4612
  color: var(--text-dim);
@@ -4504,9 +4720,13 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4504
4720
  }
4505
4721
 
4506
4722
  [data-theme="light"] .sub-win {
4507
- backdrop-filter: blur(calc(var(--glass-blur-3) * 1.35)) saturate(185%) brightness(1.12);
4508
- -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 1.35)) saturate(185%) brightness(1.12);
4509
- border-color: rgba(255,255,255,0.82);
4723
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 2.2)) saturate(165%) brightness(1.01);
4724
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 2.2)) saturate(165%) brightness(1.01);
4725
+ border-color: rgba(54,68,106,0.14);
4726
+ box-shadow: var(--modal-shadow);
4727
+ }
4728
+ [data-theme="light"] .sub-win.liquid-glass-carrier {
4729
+ background: var(--settings-modal-surface) !important;
4510
4730
  }
4511
4731
  .memory-lab-search-results[hidden] {
4512
4732
  display: none;
@@ -4521,7 +4741,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4521
4741
  align-items: center;
4522
4742
  gap: 4px;
4523
4743
  padding: 3px;
4524
- border-radius: var(--radius-md);
4744
+ border-radius: var(--radius-full);
4525
4745
  border: 1px solid var(--glass-border-1);
4526
4746
  background: var(--glass-bg-1);
4527
4747
  }
@@ -4572,7 +4792,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4572
4792
  .memory-lab-overview-actions select {
4573
4793
  height: 26px;
4574
4794
  padding: 0 9px;
4575
- border-radius: var(--radius-sm);
4795
+ border-radius: var(--radius-full);
4576
4796
  border: 1px solid transparent;
4577
4797
  background: transparent;
4578
4798
  color: var(--text-dim);
@@ -5400,6 +5620,202 @@ html.newmark-memory-overview-viewer body {
5400
5620
  opacity: 0.8;
5401
5621
  }
5402
5622
 
5623
+
5624
+ /* ============================================================
5625
+ LIQUID GLASS (Kyant AndroidLiquidGlass backdrop parity)
5626
+ CSS equivalent of the library lens/edge refraction, top
5627
+ highlight, inner shadow, outer shadow and vibrancy boost.
5628
+ ============================================================ */
5629
+ .liquid-glass {
5630
+ position: relative;
5631
+ backdrop-filter: blur(var(--glass-blur-3)) saturate(140%) brightness(1.02);
5632
+ -webkit-backdrop-filter: blur(var(--glass-blur-3)) saturate(140%) brightness(1.02);
5633
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), var(--liquid-outer-shadow);
5634
+ }
5635
+ .liquid-glass::before {
5636
+ content: '';
5637
+ position: absolute;
5638
+ inset: 0;
5639
+ pointer-events: none;
5640
+ border-radius: inherit;
5641
+ background: var(--liquid-refraction);
5642
+ opacity: 0.9;
5643
+ mix-blend-mode: screen;
5644
+ }
5645
+ .liquid-glass::after {
5646
+ content: '';
5647
+ position: absolute;
5648
+ inset: 0;
5649
+ pointer-events: none;
5650
+ border-radius: inherit;
5651
+ border: 1px solid var(--liquid-edge-glow);
5652
+ box-shadow: inset 0 0 0 0.5px var(--liquid-edge-glow-strong);
5653
+ }
5654
+ .liquid-glass-strong {
5655
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 1.5)) saturate(150%) brightness(1.04);
5656
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 1.5)) saturate(150%) brightness(1.04);
5657
+ }
5658
+ [data-theme='light'] .liquid-glass {
5659
+ backdrop-filter: none;
5660
+ -webkit-backdrop-filter: none;
5661
+ }
5662
+ [data-theme='light'] .liquid-glass::before {
5663
+ opacity: 0.45;
5664
+ mix-blend-mode: normal;
5665
+ }
5666
+ .liquid-glass-carrier {
5667
+ background: rgb(var(--glass-rgb-2) / var(--carrier-glass-alpha)) !important;
5668
+ backdrop-filter: blur(var(--carrier-glass-blur)) saturate(150%) brightness(1.02) !important;
5669
+ -webkit-backdrop-filter: blur(var(--carrier-glass-blur)) saturate(150%) brightness(1.02) !important;
5670
+ }
5671
+ /* List popups use the live carrier refraction only. The old static outer
5672
+ glow/shadow painted a second fixed background behind the popup. */
5673
+ .conv-action-menu.liquid-glass,
5674
+ .model-select-menu.liquid-glass,
5675
+ .newmark-select-menu.liquid-glass {
5676
+ box-shadow: none;
5677
+ }
5678
+ .conv-action-menu.liquid-glass::before,
5679
+ .model-select-menu.liquid-glass::before,
5680
+ .newmark-select-menu.liquid-glass::before {
5681
+ background: none;
5682
+ opacity: 0;
5683
+ mix-blend-mode: normal;
5684
+ }
5685
+ .conv-action-menu.liquid-glass::after,
5686
+ .model-select-menu.liquid-glass::after,
5687
+ .newmark-select-menu.liquid-glass::after {
5688
+ box-shadow: none;
5689
+ }
5690
+ .liquid-selection-canvas { opacity: var(--liquid-float-optical-opacity); }
5691
+
5692
+ /* Buttons retain their component-owned static surface. The Kyant-like glass
5693
+ layer exists only during press and falls away with the color block. */
5694
+ button {
5695
+ position: relative;
5696
+ isolation: isolate;
5697
+ transition-property: scale, filter, border-color, background-color, box-shadow, color;
5698
+ transition-duration: 120ms;
5699
+ transition-timing-function: var(--ease-out-expo);
5700
+ }
5701
+ [data-theme="light"] .conv-action-menu,
5702
+ [data-theme="light"] .model-select-menu,
5703
+ [data-theme="light"] .newmark-select-menu,
5704
+ [data-theme="light"] .sub-win {
5705
+ background-color: transparent;
5706
+ }
5707
+ button::after {
5708
+ content: '';
5709
+ position: absolute;
5710
+ inset: -2px;
5711
+ z-index: -1;
5712
+ pointer-events: none;
5713
+ border-radius: inherit;
5714
+ opacity: 0;
5715
+ background: var(--liquid-refraction);
5716
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
5717
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
5718
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 4px 14px rgba(0,0,0,0.18);
5719
+ transition: opacity 90ms var(--ease-out-expo);
5720
+ }
5721
+ button:hover:not(:disabled) {
5722
+ filter: brightness(1.06);
5723
+ }
5724
+ button:active:not(:disabled) {
5725
+ scale: 1.03;
5726
+ filter: brightness(1.04);
5727
+ background-color: transparent !important;
5728
+ box-shadow: none;
5729
+ }
5730
+ button:active:not(:disabled)::after { opacity: 1; }
5731
+ button:disabled { opacity: 0.5; }
5732
+
5733
+ /* Borderless utility actions stay visually quiet; they never gain a glass plate. */
5734
+ .msg-action-btn::after,
5735
+ .top-btn::after,
5736
+ .conv-more-btn::after,
5737
+ .provider-icon-btn::after,
5738
+ .work-review-toggle::after,
5739
+ button.conversation-work-run-head::after,
5740
+ button.shell-block-header::after,
5741
+ button.diff-block-header::after,
5742
+ button.work-review-head::after { display: none !important; }
5743
+ button.conversation-work-run-head:active:not(:disabled),
5744
+ button.shell-block-header:active:not(:disabled),
5745
+ button.diff-block-header:active:not(:disabled),
5746
+ button.work-review-head:active:not(:disabled) {
5747
+ scale: 1;
5748
+ filter: none;
5749
+ background-color: revert !important;
5750
+ }
5751
+
5752
+ /* Two-position liquid track. The thumb becomes glass only while held. */
5753
+ input[type="checkbox"] {
5754
+ --liquid-switch-x: 0px;
5755
+ --liquid-switch-progress: 0%;
5756
+ appearance: none;
5757
+ -webkit-appearance: none;
5758
+ width: 42px !important;
5759
+ height: 24px !important;
5760
+ min-width: 42px;
5761
+ margin: 0;
5762
+ padding: 0;
5763
+ border: 1px solid var(--glass-border-2);
5764
+ border-radius: var(--radius-full);
5765
+ background: linear-gradient(90deg, var(--accent) 0 var(--liquid-switch-progress), var(--glass-bg-3) var(--liquid-switch-progress) 100%);
5766
+ cursor: pointer;
5767
+ position: relative;
5768
+ vertical-align: middle;
5769
+ transition: background 160ms var(--ease-out-expo), border-color 160ms var(--ease-out-expo);
5770
+ }
5771
+ input[type="checkbox"]:checked {
5772
+ --liquid-switch-x: 18px;
5773
+ --liquid-switch-progress: 100%;
5774
+ border-color: color-mix(in srgb, var(--accent) 72%, white);
5775
+ }
5776
+ input[type="checkbox"]::before {
5777
+ content: '';
5778
+ position: absolute;
5779
+ left: 2px;
5780
+ top: 2px;
5781
+ width: 18px;
5782
+ height: 18px;
5783
+ border-radius: 50%;
5784
+ background: var(--text-bright);
5785
+ box-shadow: 0 2px 5px rgba(0,0,0,.28);
5786
+ transform: translateX(var(--liquid-switch-x)) scale(1);
5787
+ transition: transform 160ms var(--ease-out-expo), box-shadow 100ms var(--ease-out-expo), background 100ms var(--ease-out-expo);
5788
+ }
5789
+ input[type="checkbox"].liquid-switch-dragging::before {
5790
+ left: 1px;
5791
+ width: 22px;
5792
+ border-radius: var(--radius-full);
5793
+ background: var(--liquid-refraction);
5794
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
5795
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
5796
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 7px 18px rgba(0,0,0,.32);
5797
+ transform: translateX(var(--liquid-switch-x)) translateY(-2px) scale(1.22);
5798
+ transition: none;
5799
+ }
5800
+
5801
+ /* Center composer and terminal stay solid. Only their left/right edge catches
5802
+ light so the glass sidebars remain the visual frame. */
5803
+ #input-area,
5804
+ #bottom {
5805
+ backdrop-filter: none;
5806
+ -webkit-backdrop-filter: none;
5807
+ box-shadow: inset 1px 0 0 var(--liquid-edge-glow), inset -1px 0 0 var(--liquid-edge-glow);
5808
+ }
5809
+
5810
+ /* The live indicator is strictly a continuous black/white/black/white border.
5811
+ Keep it above generic glass pseudo-elements when classes are combined. */
5812
+ .marquee-border::before,
5813
+ .liquid-glass.marquee-border::before {
5814
+ background: conic-gradient(from var(--marquee-angle, 0deg), #000 0deg, #fff 90deg, #000 180deg, #fff 270deg, #000 360deg);
5815
+ opacity: 1;
5816
+ mix-blend-mode: normal;
5817
+ animation: marquee-rotate var(--marquee-speed) linear infinite;
5818
+ }
5403
5819
  </style>
5404
5820
  </head>
5405
5821
  <body>
@@ -5625,6 +6041,10 @@ html.newmark-memory-overview-viewer body {
5625
6041
  <symbol id="minus" viewBox="0 0 24 24">
5626
6042
  <path d="M5 12h14" />
5627
6043
  </symbol>
6044
+ <symbol id="mouse" viewBox="0 0 24 24">
6045
+ <rect x="5" y="2" width="14" height="20" rx="7" />
6046
+ <path d="M12 6v4" />
6047
+ </symbol>
5628
6048
  <symbol id="move" viewBox="0 0 24 24">
5629
6049
  <path d="M12 2v20" />
5630
6050
  <path d="m15 19-3 3-3-3" />
@@ -5875,6 +6295,7 @@ html.newmark-memory-overview-viewer body {
5875
6295
  <button type="button" class="lt-item" id="lt-ws-icon" onclick="window.toggleSecondarySidebar()" title="Workspaces"><svg class="nm-icon small" aria-hidden="true" focusable="false"><use href="#folder-open"></use></svg><span class="icon-only-label">Workspaces</span></button>
5876
6296
  </div>
5877
6297
  <div id="left-content">
6298
+ <div id="left-tool-surface">
5878
6299
  <div style="padding:10px 12px 4px;font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;flex-shrink:0;">Tools</div>
5879
6300
  <button type="button" class="left-nav-icon" onclick="window.showNewConversationPage()" title="New conversation">
5880
6301
  <span class="icon"><svg class="nm-icon small" aria-hidden="true" focusable="false"><use href="#message-square"></use></svg></span><span>New chat</span>
@@ -5894,6 +6315,7 @@ html.newmark-memory-overview-viewer body {
5894
6315
  <button type="button" class="left-nav-icon" onclick="window.openSettings()">
5895
6316
  <span class="icon"><svg class="nm-icon small" aria-hidden="true" focusable="false"><use href="#settings"></use></svg></span><span>Settings</span>
5896
6317
  </button>
6318
+ </div>
5897
6319
  <div id="left-ws-section">
5898
6320
  <div id="left-ws-header">Workspaces</div>
5899
6321
  <div id="left-ws-list" role="list" aria-label="Workspaces"></div>
@@ -5993,9 +6415,9 @@ html.newmark-memory-overview-viewer body {
5993
6415
  <div class="model-select-shell" id="model-select-shell">
5994
6416
  <select class="model-select-native" id="model-select" tabindex="-1" aria-hidden="true"></select>
5995
6417
  <button type="button" class="tool-select model-select-button" id="model-select-button" aria-haspopup="listbox" aria-expanded="false" onclick="window.toggleModelSelectMenu()" onkeydown="window.handleModelSelectButtonKey(event)"><span class="model-select-button-label" id="model-select-label"></span><svg class="nm-icon tiny" aria-hidden="true" focusable="false"><use href="#chevron-down"></use></svg></button>
5996
- <div class="model-select-menu" id="model-select-menu" role="listbox" aria-label="Model" popover="manual"></div>
6418
+ <div class="model-select-menu liquid-glass liquid-glass-carrier" id="model-select-menu" role="listbox" aria-label="Model" popover="manual"></div>
5997
6419
  </div>
5998
- <div class="conv-action-menu" id="conversation-action-menu" role="menu" aria-label="Conversation actions" popover="manual"></div>
6420
+ <div class="conv-action-menu liquid-glass liquid-glass-carrier" id="conversation-action-menu" role="menu" aria-label="Conversation actions" popover="manual"></div>
5999
6421
  <div class="context-token-wrap">
6000
6422
  <button type="button" id="context-token-ring" title="Context tokens" aria-haspopup="dialog" aria-expanded="false" onclick="window.toggleContextInspector()" onmouseenter="window.showContextWindowTooltip()" onmouseleave="window.hideContextWindowTooltip()" onfocus="window.showContextWindowTooltip()" onblur="window.hideContextWindowTooltip()"><span class="icon-only-label">Context tokens</span></button>
6001
6423
  <div id="context-token-tooltip"></div>
@@ -6113,7 +6535,7 @@ html.newmark-memory-overview-viewer body {
6113
6535
 
6114
6536
  <!-- ========== SUB-WINDOW OVERLAY ========== -->
6115
6537
  <div class="sub-win-overlay" id="sub-win-overlay" onclick="if(event.target===this)window.closeSubWin()">
6116
- <div class="sub-win" id="sub-win" role="dialog" aria-modal="true" aria-labelledby="sub-win-title" tabindex="-1">
6538
+ <div class="sub-win liquid-glass liquid-glass-carrier" id="sub-win" role="dialog" aria-modal="true" aria-labelledby="sub-win-title" tabindex="-1">
6117
6539
  <div class="sub-win-header" id="sub-win-header">
6118
6540
  <span class="sub-win-title" id="sub-win-title">Window</span>
6119
6541
  <button class="sub-win-close" onclick="window.closeSubWin()" title="Close"><svg class="nm-icon small" aria-hidden="true" focusable="false"><use href="#x"></use></svg><span class="icon-only-label">Close</span></button>
@@ -6292,6 +6714,7 @@ var state = {
6292
6714
  activeSendCallsByTarget: {},
6293
6715
  backendQueue: { steering: [], followUp: [] },
6294
6716
  backendQueuesByTarget: {},
6717
+ queueItemsByTarget: {},
6295
6718
  defaultFlow: '',
6296
6719
  settingsActiveTab: 'general',
6297
6720
  globalAgentPrompt: '',
@@ -7077,6 +7500,7 @@ var NEWMARK_I18N = {
7077
7500
  'conversation.branchCommunicationBadge': 'Branch communication',
7078
7501
  'conversation.loadingIsolated': 'Loading isolated conversation...',
7079
7502
  'conversation.locked': 'Current conversation is locked while the agent is working.',
7503
+ 'conversation.emptyWorkspace': 'No conversations in this workspace yet.',
7080
7504
  'queue.next': 'Next',
7081
7505
  'terminal.notConnected': 'Terminal not connected',
7082
7506
  'common.error': 'Error',
@@ -7808,6 +8232,7 @@ var NEWMARK_I18N = {
7808
8232
  'conversation.branchCommunicationBadge': '分支交流',
7809
8233
  'conversation.loadingIsolated': '正在加载隔离对话...',
7810
8234
  'conversation.locked': 'Agent 工作中,当前对话已锁定。',
8235
+ 'conversation.emptyWorkspace': '当前工作区还没有对话。',
7811
8236
  'queue.next': '下一轮',
7812
8237
  'terminal.notConnected': '终端未连接',
7813
8238
  'common.error': '错误',
@@ -7920,9 +8345,9 @@ function applyLanguageToUi() {
7920
8345
  var nextBtn = document.querySelector('.mode-toggle-btn[data-mode="next"]');
7921
8346
  if (guideBtn) guideBtn.textContent = t('input.guide');
7922
8347
  if (nextBtn) nextBtn.textContent = t('input.next');
7923
- var toolsHead = document.querySelector('#left-content > div:first-child');
8348
+ var toolsHead = document.querySelector('#left-tool-surface > div:first-child');
7924
8349
  if (toolsHead) toolsHead.textContent = t('left.tools');
7925
- var navs = document.querySelectorAll('#left-content .left-nav-icon');
8350
+ var navs = document.querySelectorAll('#left-tool-surface .left-nav-icon');
7926
8351
  var navLabels = [t('left.newChat'), t('left.plugins'), t('left.memoryLab'), t('left.automation'), t('left.flow'), t('left.settings')];
7927
8352
  for (var i = 0; i < navs.length && i < navLabels.length; i++) {
7928
8353
  var span = navs[i].querySelector('span:last-child');
@@ -8395,6 +8820,30 @@ function syncTerminalShellOptions() {
8395
8820
 
8396
8821
  // === API Bridge ===
8397
8822
  var api = window.api || {
8823
+ queueAction: async function(action, input) {
8824
+ try {
8825
+ var target = normalizeQueuedConversationTarget(currentConversationTarget());
8826
+ if (!target) return { ok: false, error: 'No active conversation target' };
8827
+ var body = {
8828
+ workspaceId: target.workspaceId,
8829
+ conversationId: target.conversationId,
8830
+ action: action,
8831
+ id: String(input && input.id || ''),
8832
+ text: String(input && input.text || ''),
8833
+ value: String(input && input.text || ''),
8834
+ requestedMode: String(input && input.requestedMode || 'build'),
8835
+ goalObjective: String(input && input.goalObjective || ''),
8836
+ createdAt: String(input && input.createdAt || ''),
8837
+ };
8838
+ if (Array.isArray(input && input.orderedIds)) body.orderedIds = input.orderedIds;
8839
+ var r = await fetch('/api/queue-action', {
8840
+ method: 'POST',
8841
+ headers: { 'Content-Type': 'application/json' },
8842
+ body: JSON.stringify(body)
8843
+ });
8844
+ return r.json();
8845
+ } catch(e) { return { ok: false, error: e.message }; }
8846
+ },
8398
8847
  sendMessage: async function(msg, conversationId) {
8399
8848
  try {
8400
8849
  var body = {message: msg, mode: state.mode, model: state.model, intelligence: state.intelligence, inputMode: state.inputMode};
@@ -9316,24 +9765,84 @@ window.openWorkReviewFile = async function(path) {
9316
9765
  return true;
9317
9766
  };
9318
9767
 
9319
- function addWorkReview(diffs) {
9768
+ function workReviewRunIds(runId, aliases) {
9769
+ var ids = [];
9770
+ [runId].concat(Array.isArray(aliases) ? aliases : []).forEach(function(value) {
9771
+ var id = String(value || '');
9772
+ if (id && ids.indexOf(id) < 0) ids.push(id);
9773
+ });
9774
+ return ids;
9775
+ }
9776
+
9777
+ function upsertWorkReviewRecord(ui, diffs, runId, aliases) {
9778
+ if (!ui) return null;
9779
+ if (!Array.isArray(ui.workReviews)) ui.workReviews = [];
9780
+ var ids = workReviewRunIds(runId, aliases);
9781
+ var canonicalRunId = String(runId || ids[0] || '');
9782
+ var record = { diffs: diffs, runId: canonicalRunId };
9783
+ var matchIndex = -1;
9784
+ for (var index = 0; index < ui.workReviews.length; index++) {
9785
+ var itemRunId = String(ui.workReviews[index] && ui.workReviews[index].runId || '');
9786
+ if (ids.indexOf(itemRunId) < 0) continue;
9787
+ if (matchIndex < 0) {
9788
+ matchIndex = index;
9789
+ ui.workReviews[index] = record;
9790
+ } else {
9791
+ ui.workReviews.splice(index, 1);
9792
+ index--;
9793
+ }
9794
+ }
9795
+ if (matchIndex < 0) ui.workReviews.push(record);
9796
+ return record;
9797
+ }
9798
+
9799
+ function addWorkReview(diffs, runId, aliases) {
9320
9800
  var files = normalizeWorkReviewDiffs(diffs);
9321
9801
  if (!files.length) return null;
9802
+ var filesKey = JSON.stringify(files);
9803
+ var reviewRunId = String(runId || '');
9804
+ var reviewRunIds = workReviewRunIds(reviewRunId, aliases);
9805
+ var chatArea = els['chat-area'];
9806
+ var review = null;
9807
+ if (chatArea) {
9808
+ var existingReviews = chatArea.querySelectorAll('.work-review');
9809
+ for (var ri = 0; ri < existingReviews.length; ri++) {
9810
+ var sameFiles = existingReviews[ri].getAttribute('data-files') === filesKey;
9811
+ var existingRunId = String(existingReviews[ri].getAttribute('data-run-id') || '');
9812
+ var sameRun = reviewRunIds.length ? reviewRunIds.indexOf(existingRunId) >= 0 : sameFiles && !existingRunId;
9813
+ if (!sameRun) continue;
9814
+ if (!review) review = existingReviews[ri];
9815
+ else existingReviews[ri].remove();
9816
+ }
9817
+ }
9322
9818
  var added = files.reduce(function(total, file) { return total + file.added; }, 0);
9323
9819
  var deleted = files.reduce(function(total, file) { return total + file.deleted; }, 0);
9324
- var review = document.createElement('div');
9325
- review.className = 'work-review collapsed';
9820
+ var wasCollapsed = review ? review.classList.contains('collapsed') : true;
9821
+ var wasExpanded = review ? review.classList.contains('expanded') : false;
9822
+ if (!review) review = document.createElement('div');
9823
+ review.className = 'work-review' + (wasCollapsed ? ' collapsed' : '') + (wasExpanded ? ' expanded' : '');
9326
9824
  review.setAttribute('data-files', JSON.stringify(files));
9825
+ if (reviewRunId) review.setAttribute('data-run-id', reviewRunId);
9826
+ else review.removeAttribute('data-run-id');
9327
9827
  var rows = files.map(function(file, index) {
9328
- return '<div class="work-review-file"' + (index >= 3 ? ' style="display:none"' : '') + '><span class="work-review-path">' + esc(file.path) + '</span><span><span class="work-review-add">+' + file.added + '</span><span class="work-review-del">-' + file.deleted + '</span></span></div>';
9828
+ return '<div class="work-review-file"' + (!wasExpanded && index >= 3 ? ' style="display:none"' : '') + '><span class="work-review-path">' + esc(file.path) + '</span><span><span class="work-review-add">+' + file.added + '</span><span class="work-review-del">-' + file.deleted + '</span></span></div>';
9329
9829
  }).join('');
9330
9830
  var editedLabel = files.length === 1 ? t('review.editedOne') : t('review.editedMany').replace('{count}', files.length);
9331
9831
  review.innerHTML = '<div class="work-review-head" onclick="window.toggleWorkReview(this)"><div class="work-review-mark">' + iconSvg('file-diff', t('review.fileChanges'), 'small') + '</div>' +
9332
9832
  '<div><div class="work-review-title">' + esc(editedLabel) + '</div><div class="work-review-stats"><span class="work-review-add">+' + added + '</span><span class="work-review-del">-' + deleted + '</span></div></div>' +
9333
9833
  '<div class="work-review-actions"><button class="work-review-btn" onclick="window.openWorkReview(this);event.stopPropagation()">' + esc(t('review.open')) + '</button>' +
9334
- '<button type="button" class="work-review-toggle" aria-expanded="false" onclick="event.stopPropagation();window.toggleWorkReview(this)" title="' + escAttr(t('queue.expand')) + '"><span class="work-review-chevron" aria-hidden="true"></span><span class="icon-only-label">' + esc(t('queue.expand')) + '</span></button></div></div>' +
9335
- '<div class="work-review-list">' + rows + (files.length > 3 ? '<button class="work-review-more" onclick="window.toggleWorkReviewFiles(this)">' + esc(t('review.showMore').replace('{count}', files.length - 3)) + '</button>' : '') + '</div>';
9336
- els['chat-area'].appendChild(review);
9834
+ '<button type="button" class="work-review-toggle" aria-expanded="' + (wasCollapsed ? 'false' : 'true') + '" onclick="event.stopPropagation();window.toggleWorkReview(this)" title="' + escAttr(t('queue.expand')) + '"><span class="work-review-chevron" aria-hidden="true"></span><span class="icon-only-label">' + esc(t('queue.expand')) + '</span></button></div></div>' +
9835
+ '<div class="work-review-list">' + rows + (files.length > 3 ? '<button class="work-review-more" onclick="window.toggleWorkReviewFiles(this)">' + esc(wasExpanded ? t('queue.collapse') : t('review.showMore').replace('{count}', files.length - 3)) + '</button>' : '') + '</div>';
9836
+ var owningRun = null;
9837
+ var renderedRuns = reviewRunId ? els['chat-area'].querySelectorAll('.conversation-work-run[data-run-id]') : [];
9838
+ for (var wi = 0; wi < renderedRuns.length; wi++) {
9839
+ if (renderedRuns[wi].getAttribute('data-run-id') === reviewRunId) { owningRun = renderedRuns[wi]; break; }
9840
+ }
9841
+ var owningMessage = owningRun && owningRun.closest ? owningRun.closest('.work-run-message') : null;
9842
+ if (!review.parentNode) {
9843
+ if (owningMessage && owningMessage.parentNode === els['chat-area']) owningMessage.insertAdjacentElement('afterend', review);
9844
+ else els['chat-area'].appendChild(review);
9845
+ }
9337
9846
  autoScrollIfAtBottom();
9338
9847
  return review;
9339
9848
  }
@@ -9701,12 +10210,25 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
9701
10210
  persistedGuideIds = persistedGuideIds || {};
9702
10211
  var messages = guideMessagesForTarget(target);
9703
10212
  var pending = Object.keys(messages).map(function(id) { return messages[id]; }).filter(function(item) {
9704
- return item && !persistedGuideIds[item.clientMessageId];
10213
+ if (!item || persistedGuideIds[item.clientMessageId]) return false;
10214
+ // A Guide already owned by its work run is rendered by the Build block
10215
+ // (expanded events or collapsed guide list), never as a separate trailing
10216
+ // message. Without this guard, collapse/expand rebuilds would re-add the
10217
+ // applied Guide at the end of the chat.
10218
+ if (workRunOwnsGuide(item.runId, item.clientMessageId, target)) return false;
10219
+ return true;
9705
10220
  }).sort(function(a, b) {
9706
10221
  return String(a.createdAt || '').localeCompare(String(b.createdAt || ''));
9707
10222
  });
9708
10223
  pending.forEach(function(item) {
9709
10224
  var element = findGuideMessageElement(item.clientMessageId);
10225
+ if (element && !item.awaitingAck && item.runId && workRunOwnsGuide(item.runId, item.clientMessageId, target)) {
10226
+ // The Guide is owned by its work run but a stale standalone copy exists
10227
+ // (e.g. created before this guard). Remove it so the Build block stays
10228
+ // the single source of truth.
10229
+ element.remove();
10230
+ element = null;
10231
+ }
9710
10232
  if (item.awaitingAck && item.runId) {
9711
10233
  var runElement = findWorkRunElement({ runId: item.runId });
9712
10234
  var runBody = runElement && runElement.querySelector('.conversation-work-run-body');
@@ -9738,6 +10260,22 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
9738
10260
  return pending.length;
9739
10261
  }
9740
10262
 
10263
+ function workRunOwnsGuide(runId, clientMessageId, target) {
10264
+ if (!runId || !clientMessageId) return false;
10265
+ var runs = workRunsForTarget(target || currentConversationTarget());
10266
+ var run = runs.find(function(item) { return String(item.runId || '') === String(runId); });
10267
+ if (!run) return false;
10268
+ function matches(guide) {
10269
+ return guide && String(guide.clientMessageId || '') === String(clientMessageId);
10270
+ }
10271
+ if ((run.guides || []).some(matches)) return true;
10272
+ return (run.events || []).some(function(event) {
10273
+ if (!event) return false;
10274
+ if (matches(event.guide)) return true;
10275
+ return !!(event.clientMessageId && String(event.clientMessageId) === String(clientMessageId));
10276
+ });
10277
+ }
10278
+
9741
10279
  function eventRuntimeKey(event) {
9742
10280
  var target = {
9743
10281
  workspaceId: event && (event.workspaceId || (event.target && event.target.workspaceId)),
@@ -10508,23 +11046,29 @@ function workRunGuideEvents(run) {
10508
11046
  });
10509
11047
  }
10510
11048
 
10511
- function renderWorkRunEvents(run, includeGuides) {
11049
+ function presentedWorkRunEvents(run, includeGuides) {
10512
11050
  var rawEvents = (run.events || []).filter(publicWorkEvent).filter(function(event) {
10513
11051
  return includeGuides !== false || !(String(event && event.type || '').toLowerCase().indexOf('guide') === 0 || !!(event && event.guide));
10514
11052
  }).sort(compareConversationWorkEvents);
10515
11053
  var terminalInterrupted = ['interrupted', 'force_interrupted'].indexOf(String(run && run.status || '')) >= 0;
10516
11054
  var events = [];
10517
11055
  var publicText = '';
11056
+ // 公共文本段继承其最后一条 text 事件的 sequence:增量渲染依赖 presented
11057
+ // 顺序的单调性,无 sequence 的文本段会被排到列表前部,导致每次文本增量
11058
+ // 都从前部重建后续节点。
11059
+ var publicTextSequence = 0;
10518
11060
  function flushPublicText() {
10519
11061
  var content = publicText.trim().slice(0, 50000);
10520
11062
  publicText = '';
10521
- if (content) events.push({ type: terminalInterrupted ? 'partial_text' : 'public_text', content: content });
11063
+ if (content) events.push({ type: terminalInterrupted ? 'partial_text' : 'public_text', content: content, sequence: publicTextSequence });
11064
+ publicTextSequence = 0;
10522
11065
  }
10523
11066
  for (var eventIndex = 0; eventIndex < rawEvents.length; eventIndex++) {
10524
11067
  var rawEvent = rawEvents[eventIndex];
10525
11068
  var rawType = String(rawEvent && rawEvent.type || '').toLowerCase();
10526
11069
  if (rawType === 'text') {
10527
11070
  publicText += String(rawEvent.content || '');
11071
+ publicTextSequence = Math.max(publicTextSequence, Number(rawEvent.sequence || 0));
10528
11072
  continue;
10529
11073
  }
10530
11074
  if (rawType === 'response') {
@@ -10596,46 +11140,120 @@ function renderWorkRunEvents(run, includeGuides) {
10596
11140
  }
10597
11141
  presentedEvents.push({ type: 'tool_group', count: count, completed: allCompleted, toolName: presented.toolName, items: activityItems });
10598
11142
  }
10599
- events = presentedEvents;
10600
- if (!events.length) return '<div class="conversation-work-empty">' + esc(currentLang() === 'zh' ? '等待公开工作进度…' : 'Waiting for public progress…') + '</div>';
10601
- return '<div class="conversation-work-run-events">' + events.map(function(event, eventIndex) {
10602
- var type = String(event.type || 'status').toLowerCase();
10603
- if (type === 'public_text' || type === 'partial_text' || type === 'response') {
10604
- var responseLabel = type === 'response' && terminalInterrupted
10605
- ? (currentLang() === 'zh' ? '未完成回复片段\n' : 'Incomplete response fragment\n') + workEventLabel(event)
10606
- : workEventLabel(event);
10607
- // While a run is still receiving text, this view can be rebuilt once per
10608
- // event. Avoid invoking the full Markdown parser for a large live
10609
- // narrative; the final response path still gets the normal rich render.
10610
- var liveNarrative = ['running', 'stopping', 'force_restarting'].indexOf(String(run && run.status || '').toLowerCase()) >= 0;
10611
- var narrativeHtml = liveNarrative && String(responseLabel || '').length >= STREAMING_PLAIN_TEXT_THRESHOLD
10612
- ? esc(responseLabel)
10613
- : renderMessageContent(responseLabel);
10614
- return '<div class="conversation-work-event narrative"><div class="conversation-work-event-content">' + narrativeHtml + '</div></div>';
10615
- }
10616
- if (type.indexOf('guide') === 0 || event.guide) return renderWorkRunGuideMessage(event);
10617
- if (type === 'tool_group') return renderWorkToolGroup(event, eventIndex);
10618
- if (type === 'thought') return renderWorkThought(event, eventIndex);
10619
- var activitySummary = type.indexOf('tool_') === 0 && type !== 'tool_call' && type !== 'tool_result';
10620
- var cls = type.indexOf('guide') === 0 ? ' guide' : (type === 'error' ? ' error' : (activitySummary ? ' activity-summary' : ''));
10621
- var label = activitySummary ? workToolActivityLabel(event.activity, Number(event.count || 1), !!event.completed) : workEventLabel(event) + (event.completed ? (currentLang() === 'zh' ? ' · 已完成' : ' · completed') : '');
10622
- if (activitySummary) {
10623
- var firstActivityItem = event.items && event.items[0] || {};
10624
- var activityDomKey = String(firstActivityItem.id || firstActivityItem.toolCallId || ((event.activity && event.activity.key || type) + '-' + eventIndex));
10625
- var detailRows = (event.items || []).map(function(item) {
10626
- var detail = String(item.toolArgs || '').trim();
10627
- return '<div class="conversation-work-activity-item">' + iconSvg(workEventIcon('tool_call'), 'tool_call', 'tiny') +
10628
- '<div><div>' + esc(publicToolNameForUi(item.toolName)) + (item.completed ? (currentLang() === 'zh' ? ' · 已完成' : ' · completed') : '') + '</div>' +
10629
- (detail ? '<div class="conversation-work-activity-detail">' + esc(detail) + '</div>' : '') + '</div></div>';
10630
- }).join('');
10631
- return '<details class="conversation-work-activity" data-activity-key="' + escAttr(activityDomKey) + '"><summary><span class="conversation-work-activity-chevron" aria-hidden="true"></span><div>' + esc(label) + '</div></summary>' +
10632
- '<div class="conversation-work-activity-list">' + detailRows + '</div></details>';
10633
- }
10634
- return '<div class="conversation-work-event' + cls + '">' + iconSvg(workEventIcon(type), type, 'tiny') +
10635
- '<div class="conversation-work-event-content">' + renderMessageContent(label) + '</div></div>';
11143
+ return { events: presentedEvents, terminalInterrupted: terminalInterrupted };
11144
+ }
11145
+
11146
+ // 每个呈现节点的稳定 key:内容变化(增长/完成)时 key 变化,增量渲染据此
11147
+ // 只重建变化节点之后的 DOM,前缀节点原样保留。
11148
+ function workRunPresentedEventKey(event, eventIndex) {
11149
+ var type = String(event && event.type || 'status').toLowerCase();
11150
+ if (type === 'public_text' || type === 'partial_text' || type === 'response') {
11151
+ return type + ':' + String(event && event.content || '').length;
11152
+ }
11153
+ if (type === 'thought') {
11154
+ return 'thought:' + String(event && (event.id || event.toolCallId) || '') + '@' + String(event && event.content || '').length + ':' + (event && event.completed ? '1' : '0');
11155
+ }
11156
+ if (type === 'tool_group') {
11157
+ return 'tool_group:' + ((event && event.items || []).map(function(item) {
11158
+ return String(item && (item.id || item.toolCallId) || '') + ':' + (item && item.completed ? '1' : '0');
11159
+ }).join(',')) + '#n' + Number(event && event.count || 0);
11160
+ }
11161
+ if (type.indexOf('guide') === 0 || (event && event.guide)) {
11162
+ return 'guide:' + guideWorkEventKey(event) + ':' + guideWorkEventStatus(event);
11163
+ }
11164
+ var activityTail = type.indexOf('tool_') === 0 && type !== 'tool_call' && type !== 'tool_result'
11165
+ ? ':' + String(event && event.activity && event.activity.key || '') + ':' + Number(event && event.count || 1)
11166
+ : '';
11167
+ return type + ':' + String(event && (event.id || [event && event.sequence || '', event && event.toolName || '', event && event.timestamp || ''].join('|')) || '') + ':' + (event && event.completed ? '1' : '0') + activityTail;
11168
+ }
11169
+
11170
+ function renderWorkRunEventNode(event, eventIndex, run, terminalInterrupted) {
11171
+ var type = String(event.type || 'status').toLowerCase();
11172
+ if (type === 'public_text' || type === 'partial_text' || type === 'response') {
11173
+ var responseLabel = type === 'response' && terminalInterrupted
11174
+ ? (currentLang() === 'zh' ? '未完成回复片段\n' : 'Incomplete response fragment\n') + workEventLabel(event)
11175
+ : workEventLabel(event);
11176
+ // While a run is still receiving text, this view can be rebuilt once per
11177
+ // event. Avoid invoking the full Markdown parser for a large live
11178
+ // narrative; the final response path still gets the normal rich render.
11179
+ var liveNarrative = ['running', 'stopping', 'force_restarting'].indexOf(String(run && run.status || '').toLowerCase()) >= 0;
11180
+ var narrativeHtml = liveNarrative && String(responseLabel || '').length >= STREAMING_PLAIN_TEXT_THRESHOLD
11181
+ ? esc(responseLabel)
11182
+ : renderMessageContent(responseLabel);
11183
+ return '<div class="conversation-work-event narrative"><div class="conversation-work-event-content">' + narrativeHtml + '</div></div>';
11184
+ }
11185
+ if (type.indexOf('guide') === 0 || event.guide) return renderWorkRunGuideMessage(event);
11186
+ if (type === 'tool_group') return renderWorkToolGroup(event, eventIndex);
11187
+ if (type === 'thought') return renderWorkThought(event, eventIndex);
11188
+ var activitySummary = type.indexOf('tool_') === 0 && type !== 'tool_call' && type !== 'tool_result';
11189
+ var cls = type.indexOf('guide') === 0 ? ' guide' : (type === 'error' ? ' error' : (activitySummary ? ' activity-summary' : ''));
11190
+ var label = activitySummary ? workToolActivityLabel(event.activity, Number(event.count || 1), !!event.completed) : workEventLabel(event) + (event.completed ? (currentLang() === 'zh' ? ' · 已完成' : ' · completed') : '');
11191
+ if (activitySummary) {
11192
+ var firstActivityItem = event.items && event.items[0] || {};
11193
+ var activityDomKey = String(firstActivityItem.id || firstActivityItem.toolCallId || ((event.activity && event.activity.key || type) + '-' + eventIndex));
11194
+ var detailRows = (event.items || []).map(function(item) {
11195
+ var detail = String(item.toolArgs || '').trim();
11196
+ return '<div class="conversation-work-activity-item">' + iconSvg(workEventIcon('tool_call'), 'tool_call', 'tiny') +
11197
+ '<div><div>' + esc(publicToolNameForUi(item.toolName)) + (item.completed ? (currentLang() === 'zh' ? ' · 已完成' : ' · completed') : '') + '</div>' +
11198
+ (detail ? '<div class="conversation-work-activity-detail">' + esc(detail) + '</div>' : '') + '</div></div>';
11199
+ }).join('');
11200
+ return '<details class="conversation-work-activity" data-activity-key="' + escAttr(activityDomKey) + '"><summary><span class="conversation-work-activity-chevron" aria-hidden="true"></span><div>' + esc(label) + '</div></summary>' +
11201
+ '<div class="conversation-work-activity-list">' + detailRows + '</div></details>';
11202
+ }
11203
+ return '<div class="conversation-work-event' + cls + '">' + iconSvg(workEventIcon(type), type, 'tiny') +
11204
+ '<div class="conversation-work-event-content">' + renderMessageContent(label) + '</div></div>';
11205
+ }
11206
+
11207
+ // 给单个事件节点的 HTML 根标签插入稳定 key,使首次渲染的节点与增量
11208
+ // diff 使用同一 key 体系(否则第一次增量更新会全量失配重建)。
11209
+ function withWorkEventKey(html, key) {
11210
+ var match = /^<([A-Za-z][\w-]*)([^>]*)>/.exec(String(html || ''));
11211
+ if (!match) return html;
11212
+ return '<' + match[1] + match[2] + ' data-work-event-key="' + escAttr(String(key || '')) + '">' + html.slice(match[0].length);
11213
+ }
11214
+
11215
+ function renderWorkRunEvents(run, includeGuides) {
11216
+ var presented = presentedWorkRunEvents(run, includeGuides);
11217
+ if (!presented.events.length) return '<div class="conversation-work-empty">' + esc(currentLang() === 'zh' ? '等待公开工作进度…' : 'Waiting for public progress…') + '</div>';
11218
+ return '<div class="conversation-work-run-events">' + presented.events.map(function(event, eventIndex) {
11219
+ return withWorkEventKey(renderWorkRunEventNode(event, eventIndex, run, presented.terminalInterrupted), workRunPresentedEventKey(event, eventIndex));
10636
11220
  }).join('') + '</div>';
10637
11221
  }
10638
11222
 
11223
+ // 增量更新 run body 内的事件列表:保留与新呈现列表 key 一致的前缀节点,
11224
+ // 从第一个分歧节点起重建尾部。运行中通常只有最后一个文本/thought 节点
11225
+ // 增长或尾部追加,因此不会整块重绘整个进程渲染。
11226
+ function updateWorkRunEventsContainer(run, body, expanded) {
11227
+ var presented = presentedWorkRunEvents(run, expanded);
11228
+ var container = body.querySelector ? body.querySelector('.conversation-work-run-events') : null;
11229
+ if (!container) {
11230
+ body.innerHTML = renderWorkRunEvents(run, expanded);
11231
+ return;
11232
+ }
11233
+ if (!presented.events.length) {
11234
+ var emptyHtml = '<div class="conversation-work-empty">' + esc(currentLang() === 'zh' ? '等待公开工作进度…' : 'Waiting for public progress…') + '</div>';
11235
+ if (container.innerHTML !== emptyHtml) container.innerHTML = emptyHtml;
11236
+ return;
11237
+ }
11238
+ var nodes = Array.prototype.slice.call(container.children);
11239
+ var keep = 0;
11240
+ while (keep < nodes.length && keep < presented.events.length) {
11241
+ if (String(nodes[keep].getAttribute('data-work-event-key') || '') === workRunPresentedEventKey(presented.events[keep], keep)) keep++;
11242
+ else break;
11243
+ }
11244
+ while (container.children.length > keep) container.removeChild(container.lastChild);
11245
+ for (var i = keep; i < presented.events.length; i++) {
11246
+ var key = workRunPresentedEventKey(presented.events[i], i);
11247
+ var holder = document.createElement('div');
11248
+ holder.innerHTML = renderWorkRunEventNode(presented.events[i], i, run, presented.terminalInterrupted);
11249
+ while (holder.firstChild) {
11250
+ var node = holder.firstChild;
11251
+ node.setAttribute('data-work-event-key', key);
11252
+ container.appendChild(node);
11253
+ }
11254
+ }
11255
+ }
11256
+
10639
11257
  function findWorkRunElement(run) {
10640
11258
  if (!els['chat-area']) return null;
10641
11259
  var nodes = els['chat-area'].querySelectorAll('.conversation-work-run');
@@ -10648,6 +11266,11 @@ function findWorkRunElement(run) {
10648
11266
  function updateConversationWorkRunElement(run, element) {
10649
11267
  element = element || findWorkRunElement(run);
10650
11268
  if (!element) return null;
11269
+ // 重建/增量更新前后保持聊天视窗滚动位置:贴底时跟随最新,未贴底时
11270
+ // 精确恢复 scrollTop,避免内容高度变化把视窗“强制拖向最新”。
11271
+ var chatEl = els['chat-area'];
11272
+ var wasAtBottom = !!(chatEl && shouldAutoScroll(chatEl));
11273
+ var scrollTopBefore = chatEl ? chatEl.scrollTop : 0;
10651
11274
  var activityOpenStates = {};
10652
11275
  var priorActivities = element.querySelectorAll ? element.querySelectorAll('details.conversation-work-activity[data-activity-key], details[data-work-detail-key]') : [];
10653
11276
  for (var activityIndex = 0; activityIndex < priorActivities.length; activityIndex++) {
@@ -10662,12 +11285,51 @@ function updateConversationWorkRunElement(run, element) {
10662
11285
  element.className = 'conversation-work-run ' + (running ? 'running ' : '') + (expanded ? 'expanded' : 'collapsed');
10663
11286
  element.setAttribute('data-run-id', run.runId);
10664
11287
  element.setAttribute('data-runtime-key', runtimeKeyFor(run.target && run.target.workspaceId, run.target && run.target.conversationId));
10665
- element.innerHTML = '<button class="conversation-work-run-head" type="button" onclick="window.toggleConversationWorkRun(this)">' +
10666
- '<span class="conversation-work-run-title">' + esc(workRunTitle(run)) + '</span>' +
10667
- workRunChangeBadge(run) +
10668
- '<span class="conversation-work-run-status">' + esc(String(run.status || '')) + '</span>' +
10669
- '<span class="conversation-work-run-chevron" aria-hidden="true"></span></button>' +
10670
- '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>';
11288
+ var head = element.querySelector ? element.querySelector('.conversation-work-run-head') : null;
11289
+ if (!head) {
11290
+ element.innerHTML = '<button class="conversation-work-run-head" type="button" onclick="window.toggleConversationWorkRun(this)">' +
11291
+ '<span class="conversation-work-run-title">' + esc(workRunTitle(run)) + '</span>' +
11292
+ workRunChangeBadge(run) +
11293
+ '<span class="conversation-work-run-status">' + esc(String(run.status || '')) + '</span>' +
11294
+ '<span class="conversation-work-run-chevron" aria-hidden="true"></span></button>' +
11295
+ '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>';
11296
+ } else {
11297
+ // 就地更新头部,不重建 head 按钮(保住聚焦/动画状态)。
11298
+ var titleEl = head.querySelector('.conversation-work-run-title');
11299
+ var nextTitle = workRunTitle(run);
11300
+ if (titleEl && titleEl.textContent !== nextTitle) titleEl.textContent = nextTitle;
11301
+ var statusEl = head.querySelector('.conversation-work-run-status');
11302
+ if (statusEl && statusEl.textContent !== String(run.status || '')) statusEl.textContent = String(run.status || '');
11303
+ // Keep the selector identical to workRunChangeBadge(). The old
11304
+ // `.conversation-work-run-change-badge` typo never found the existing
11305
+ // node, so every expand/collapse refresh inserted another review badge.
11306
+ var badgeHolder = head.querySelector('.conversation-work-change-badge');
11307
+ var nextBadge = workRunChangeBadge(run);
11308
+ if (nextBadge) {
11309
+ if (badgeHolder) {
11310
+ if (badgeHolder.getAttribute('data-badge-key') !== nextBadge) {
11311
+ var badgeShell = document.createElement('span');
11312
+ badgeShell.innerHTML = nextBadge;
11313
+ var replacementBadge = badgeShell.firstChild;
11314
+ replacementBadge.setAttribute('data-badge-key', nextBadge);
11315
+ badgeHolder.parentNode.replaceChild(replacementBadge, badgeHolder);
11316
+ }
11317
+ } else {
11318
+ var badgeShellNew = document.createElement('span');
11319
+ badgeShellNew.innerHTML = nextBadge;
11320
+ var badgeNode = badgeShellNew.firstChild;
11321
+ if (badgeNode) {
11322
+ badgeNode.setAttribute('data-badge-key', nextBadge);
11323
+ head.insertBefore(badgeNode, statusEl || null);
11324
+ }
11325
+ }
11326
+ } else if (badgeHolder) {
11327
+ badgeHolder.parentNode.removeChild(badgeHolder);
11328
+ }
11329
+ var body = element.querySelector('.conversation-work-run-body');
11330
+ if (body) updateWorkRunEventsContainer(run, body, expanded);
11331
+ else element.insertAdjacentHTML('beforeend', '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>');
11332
+ }
10671
11333
  renderPendingGuideMessages(run.target, {});
10672
11334
  var wrapper = element.closest ? element.closest('.work-run-message') : null;
10673
11335
  if (wrapper) {
@@ -10698,7 +11360,14 @@ function updateConversationWorkRunElement(run, element) {
10698
11360
  var refreshedKey = refreshedParentKey ? refreshedParentKey + '::' + refreshedDetailKey : '';
10699
11361
  if (activityOpenStates[refreshedKey] !== undefined) refreshedActivities[refreshedIndex].open = activityOpenStates[refreshedKey];
10700
11362
  }
10701
- autoScrollIfAtBottom();
11363
+ if (chatEl) {
11364
+ if (wasAtBottom) {
11365
+ _chatShouldAutoScroll = true;
11366
+ chatEl.scrollTop = chatEl.scrollHeight;
11367
+ } else {
11368
+ chatEl.scrollTop = scrollTopBefore;
11369
+ }
11370
+ }
10702
11371
  return element;
10703
11372
  }
10704
11373
 
@@ -10933,7 +11602,8 @@ function conversationWorkUiState(conversationId, workspaceId) {
10933
11602
  finalResponseMsg: null,
10934
11603
  finalResponseText: '',
10935
11604
  finalResponseAt: 0,
10936
- pendingWorkReview: null
11605
+ pendingWorkReview: null,
11606
+ workReviews: []
10937
11607
  };
10938
11608
  }
10939
11609
  return state.agentWorkUiByConversation[id];
@@ -10951,7 +11621,8 @@ function resetConversationWorkUi(conversationId, keepCompleted, workspaceId) {
10951
11621
  finalResponseMsg: keepCompleted ? prior.finalResponseMsg : null,
10952
11622
  finalResponseText: keepCompleted ? prior.finalResponseText : '',
10953
11623
  finalResponseAt: keepCompleted ? prior.finalResponseAt : 0,
10954
- pendingWorkReview: keepCompleted ? prior.pendingWorkReview : null
11624
+ pendingWorkReview: keepCompleted ? prior.pendingWorkReview : null,
11625
+ workReviews: keepCompleted ? (prior.workReviews || []).slice() : []
10955
11626
  };
10956
11627
  return state.agentWorkUiByConversation[id];
10957
11628
  }
@@ -11233,9 +11904,12 @@ function renderChatMessages(messages, target) {
11233
11904
  if (!orphanRoles.user && !orphanRoles.assistant) renderOwnedWorkRun(orphanRun, true);
11234
11905
  }
11235
11906
  }
11236
- var pendingWorkReview = conversationWorkUiState(renderConversationId).pendingWorkReview;
11907
+ var priorWorkUi = conversationWorkUiState(renderConversationId);
11908
+ var pendingWorkReview = priorWorkUi.pendingWorkReview;
11909
+ var retainedWorkReviews = Array.isArray(priorWorkUi.workReviews) ? priorWorkUi.workReviews.slice() : [];
11237
11910
  resetConversationWorkUi(renderConversationId, false);
11238
11911
  conversationWorkUiState(renderConversationId).pendingWorkReview = pendingWorkReview;
11912
+ conversationWorkUiState(renderConversationId).workReviews = retainedWorkReviews;
11239
11913
  if (!normalizedMessages.length) {
11240
11914
  persistedRuns.forEach(function(run) { renderOwnedWorkRun(run, true); });
11241
11915
  var pendingGuideCount = renderPendingGuideMessages(renderTarget, persistedGuideIds);
@@ -11279,7 +11953,10 @@ function renderChatMessages(messages, target) {
11279
11953
  finishToolBatch(renderConversationId);
11280
11954
  persistedRuns.forEach(function(run) { renderOwnedWorkRun(run, true); });
11281
11955
  renderPendingGuideMessages(renderTarget, persistedGuideIds);
11282
- if (pendingWorkReview) addWorkReview(pendingWorkReview);
11956
+ retainedWorkReviews.forEach(function(item) {
11957
+ addWorkReview(item.diffs || item, item.runId || '');
11958
+ });
11959
+ if (pendingWorkReview) addWorkReview(pendingWorkReview.diffs || pendingWorkReview, pendingWorkReview.runId || '');
11283
11960
  renderConversationBranchPagers();
11284
11961
  renderAutoRouteRatingControls();
11285
11962
  if (typeof renderPendingOptionsInChat === 'function') renderPendingOptionsInChat(state.pendingOptions || []);
@@ -11531,6 +12208,7 @@ function renderAgentWorkEvent(event) {
11531
12208
  var eventQueueTarget = { workspaceId: eventWorkspaceId || runtimeWorkspaceId(''), conversationId: eventConversationId };
11532
12209
  state.backendQueue = setBackendQueueForTarget(event.queue || { steering: [], followUp: [] }, eventQueueTarget);
11533
12210
  if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, eventQueueTarget);
12211
+ syncQueueItemsFromSnapshot(event, eventQueueTarget);
11534
12212
  if (window.renderInputStack) window.renderInputStack();
11535
12213
  } else {
11536
12214
  return;
@@ -11588,6 +12266,9 @@ function appendAgentWorkEvent(event) {
11588
12266
  eventRuntimeKey(event);
11589
12267
  var active = isActiveConversationTarget(target);
11590
12268
  var visibleRuntimeBranch = active && isViewingRuntimeConversationBranch(target);
12269
+ // 模型回退不是隐藏的参数回退:活跃对话发生回退时,把输入框下方的
12270
+ // 模型选择区同步为实际生效的模型。
12271
+ if (active && event.fallback && event.fallback.to) applyModelFallbackToInputSurface(event);
11591
12272
  if (active) markConversationTracked(id, state.conversationTrackMs || 300000, workspaceId);
11592
12273
  if (active || isConversationTracked(id, workspaceId)) cacheAgentWorkEvent(event);
11593
12274
  var pendingProvisionalStop = (event.type === 'start' || event.status === 'running')
@@ -11756,6 +12437,7 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
11756
12437
  state.backendQueue = setBackendQueueForTarget(s.queued, snapshotTarget);
11757
12438
  if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, snapshotTarget);
11758
12439
  }
12440
+ syncQueueItemsFromSnapshot(s, snapshotTarget);
11759
12441
  if (s && Array.isArray(s.workEvents)) {
11760
12442
  var eventConversationId = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
11761
12443
  if (!state.agentWorkEventsByConversation) state.agentWorkEventsByConversation = {};
@@ -11980,6 +12662,7 @@ window.optionManualSubmitted = function(element) {
11980
12662
 
11981
12663
  // === Working State ===
11982
12664
  function setWorking(working) {
12665
+ var changed = state.isWorking !== working;
11983
12666
  state.isWorking = working;
11984
12667
  state.activeAgentRunning = working;
11985
12668
  var btn = els['submit-btn'];
@@ -11988,13 +12671,13 @@ function setWorking(working) {
11988
12671
  var ws = els['ws-thumb'];
11989
12672
  if (working) {
11990
12673
  if (btn) btn.classList.add('working');
11991
- if (input) input.classList.add('working-glow');
12674
+ if (input) input.classList.add('working-glow', 'marquee-border');
11992
12675
  if (title) title.classList.add('animate');
11993
12676
  if (ws) ws.classList.add('marquee-border');
11994
- renderConversations();
12677
+ if (changed) renderConversations();
11995
12678
  } else {
11996
12679
  if (btn) btn.classList.remove('working');
11997
- if (input) input.classList.remove('working-glow');
12680
+ if (input) input.classList.remove('working-glow', 'marquee-border');
11998
12681
  if (title) title.classList.remove('animate');
11999
12682
  if (ws) ws.classList.remove('marquee-border');
12000
12683
  }
@@ -12534,6 +13217,7 @@ window.closeModelSelectMenu = function(options) {
12534
13217
  if (shell) shell.classList.remove('open');
12535
13218
  if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
12536
13219
  if (button) button.setAttribute('aria-expanded', 'false');
13220
+ window.setLiquidSidebarGestureLock('popup:model-select', false);
12537
13221
  if (options && options.focus && button) button.focus();
12538
13222
  };
12539
13223
 
@@ -12550,6 +13234,7 @@ window.toggleModelSelectMenu = function(forceOpen) {
12550
13234
  }
12551
13235
  if (open) document.querySelectorAll('.newmark-select-shell.open').forEach(function(item) { window.closeNewmarkSelect(item); });
12552
13236
  shell.classList.add('open');
13237
+ window.setLiquidSidebarGestureLock('popup:model-select', true);
12553
13238
  button.setAttribute('aria-expanded', 'true');
12554
13239
  if (open) {
12555
13240
  if (menu && typeof menu.showPopover === 'function' && !menu.matches(':popover-open')) menu.showPopover();
@@ -12577,6 +13262,7 @@ window.closeConversationActionMenu = function(options) {
12577
13262
  if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
12578
13263
  document.querySelectorAll('.conv-more-btn[aria-expanded="true"]').forEach(function(item) { item.setAttribute('aria-expanded', 'false'); });
12579
13264
  state.conversationActionAnchor = null;
13265
+ window.setLiquidSidebarGestureLock('popup:conversation-action', false);
12580
13266
  if (options && options.focus && anchor && anchor.focus) anchor.focus();
12581
13267
  };
12582
13268
 
@@ -12632,6 +13318,7 @@ window.openConversationActionMenu = function(anchor, conversationId, pos) {
12632
13318
  document.querySelectorAll('.conv-more-btn[aria-expanded="true"]').forEach(function(item) { item.setAttribute('aria-expanded', 'false'); });
12633
13319
  if (moreBtn) moreBtn.setAttribute('aria-expanded', 'true');
12634
13320
  state.conversationActionAnchor = anchor || moreBtn || null;
13321
+ window.setLiquidSidebarGestureLock('popup:conversation-action', true);
12635
13322
  if (typeof menu.showPopover === 'function' && !menu.matches(':popover-open')) menu.showPopover();
12636
13323
  requestAnimationFrame(function() {
12637
13324
  window.positionConversationActionMenu(state.conversationActionAnchor || moreBtn, menu, pos);
@@ -12695,6 +13382,967 @@ window.handleModelSelectButtonKey = function(event) {
12695
13382
  }
12696
13383
  };
12697
13384
 
13385
+ // 后端回退信号携带的是纯模型名(如 gpt-4o-mini),把名称映射回前端
13386
+ // 选择器的部署值(deployment:providerId:modelName),使选择区能直接选中。
13387
+ function modelDeploymentValueForName(name, providerId) {
13388
+ var modelName = String(name || '').trim();
13389
+ if (!modelName) return '';
13390
+ var providers = Array.isArray(state.providers) ? state.providers : [];
13391
+ var match = null;
13392
+ for (var pi = 0; pi < providers.length && !match; pi++) {
13393
+ var provider = providers[pi];
13394
+ if (providerId && String(provider.id || provider.name || '') !== String(providerId)) continue;
13395
+ var models = Array.isArray(provider.models) ? provider.models : [];
13396
+ for (var mi = 0; mi < models.length && !match; mi++) {
13397
+ var entryName = typeof models[mi] === 'string' ? models[mi] : (models[mi] && models[mi].name || '');
13398
+ if (entryName === modelName) match = { provider: provider, name: entryName };
13399
+ }
13400
+ }
13401
+ if (!match) return modelName;
13402
+ var providerIdValue = String(match.provider.id || match.provider.name || '');
13403
+ return 'deployment:' + encodeURIComponent(providerIdValue) + ':' + encodeURIComponent(match.name);
13404
+ }
13405
+
13406
+ // 不可用模型回退同步输入框下方选择区:固定模型选择被回退时,选择区
13407
+ // 显示实际生效的模型;Auto 模式的路由切换不污染选择区(保持 Auto 显示)。
13408
+ function applyModelFallbackToInputSurface(event) {
13409
+ var f = event && event.fallback;
13410
+ if (!f || !f.to) return;
13411
+ if (String(state.model || '') === 'auto') return;
13412
+ var nextValue = modelDeploymentValueForName(f.to, f.providerId || '');
13413
+ if (!nextValue || String(state.model || '') === nextValue) return;
13414
+ state.model = nextValue;
13415
+ state.modelLabel = '';
13416
+ // 不预置 _syncedModel:下一次发送前的 syncConversationExecutionState 会把
13417
+ // 回退模型正式同步到后端 host agent(并持久化为该对话的首选模型)。
13418
+ window.refreshModelSelect();
13419
+ window.syncModelSelectSurface();
13420
+ }
13421
+
13422
+ var liquidSidebarGestureLocks = new Set();
13423
+ window.setLiquidSidebarGestureLock = function(token, locked) {
13424
+ if (locked) liquidSidebarGestureLocks.add(String(token));
13425
+ else liquidSidebarGestureLocks.delete(String(token));
13426
+ document.body.classList.toggle('liquid-sidebar-gesture-locked', liquidSidebarGestureLocks.size > 0);
13427
+ };
13428
+ window.liquidSidebarGesturesLocked = function() { return liquidSidebarGestureLocks.size > 0; };
13429
+
13430
+ // One moving glass capsule per select menu. It starts at the existing
13431
+ // selection, fades in, follows pointer movement across options, and commits
13432
+ // the landing option on release. Static options retain their color capsule.
13433
+ var liquidBackdropFramePromise = null;
13434
+ var liquidBackdropCachedFrame = null;
13435
+ var liquidBackdropPrimeTimer = 0;
13436
+ window.__liquidBackdropPrimeReady = false;
13437
+ function decodeLiquidBackdropFrame(frame) {
13438
+ if (!frame) return Promise.resolve(null);
13439
+ if (frame.bitmap) return Promise.resolve(frame);
13440
+ var bytes = frame.bytes;
13441
+ if (bytes && Array.isArray(bytes.data)) bytes = new Uint8Array(bytes.data);
13442
+ var blobPromise = bytes
13443
+ ? Promise.resolve(new Blob([bytes], { type: frame.mimeType || 'image/jpeg' }))
13444
+ : (frame.dataUrl ? fetch(frame.dataUrl).then(function(response) { return response.blob(); }) : Promise.resolve(null));
13445
+ return blobPromise.then(function(blob) {
13446
+ if (!blob) return null;
13447
+ return createImageBitmap(blob);
13448
+ }).then(function(bitmap) {
13449
+ if (!bitmap) return null;
13450
+ frame.bitmap = bitmap;
13451
+ frame.id = frame.id || String(Date.now()) + ':' + String(frame.width || bitmap.width) + 'x' + String(frame.height || bitmap.height);
13452
+ frame.capturedAt = performance.now();
13453
+ return frame;
13454
+ });
13455
+ }
13456
+ function captureLiquidBackdropFrame(options) {
13457
+ if (!window.api || typeof window.api.captureLiquidBackdrop !== 'function') return Promise.resolve(null);
13458
+ options = options || {};
13459
+ var maxAge = Number.isFinite(options.maxAge) ? options.maxAge : 5000;
13460
+ if (!options.force && liquidBackdropCachedFrame && performance.now() - liquidBackdropCachedFrame.capturedAt <= maxAge) {
13461
+ return Promise.resolve(liquidBackdropCachedFrame);
13462
+ }
13463
+ if (!liquidBackdropFramePromise) {
13464
+ window.__liquidBackdropPrimeReady = false;
13465
+ liquidBackdropFramePromise = window.api.captureLiquidBackdrop({ width: window.innerWidth, height: window.innerHeight })
13466
+ .then(decodeLiquidBackdropFrame)
13467
+ .then(function(frame) {
13468
+ if (frame) {
13469
+ var previous = liquidBackdropCachedFrame;
13470
+ liquidBackdropCachedFrame = frame;
13471
+ prewarmKyantRenderer(frame);
13472
+ window.__liquidBackdropPrimeReady = true;
13473
+ if (previous && previous !== frame && previous.bitmap && previous.bitmap.close) previous.bitmap.close();
13474
+ }
13475
+ return frame;
13476
+ })
13477
+ .catch(function(error) {
13478
+ window.__liquidErrors = window.__liquidErrors || [];
13479
+ window.__liquidErrors.push(String(error && (error.stack || error.message) || error));
13480
+ console.warn('[KyantGlass] backdrop capture failed:', error);
13481
+ return liquidBackdropCachedFrame;
13482
+ })
13483
+ .finally(function() { liquidBackdropFramePromise = null; });
13484
+ }
13485
+ return liquidBackdropFramePromise;
13486
+ }
13487
+
13488
+ function scheduleLiquidBackdropPrime() {
13489
+ if (liquidBackdropPrimeTimer || (liquidBackdropCachedFrame && performance.now() - liquidBackdropCachedFrame.capturedAt < 5000)) return;
13490
+ liquidBackdropPrimeTimer = setTimeout(function() {
13491
+ liquidBackdropPrimeTimer = 0;
13492
+ captureLiquidBackdropFrame({ maxAge: 5000 });
13493
+ }, liquidBackdropCachedFrame ? 220 : 48);
13494
+ }
13495
+
13496
+ if (window.requestIdleCallback) {
13497
+ window.requestIdleCallback(function() { captureLiquidBackdropFrame({ maxAge: 5000 }); }, { timeout: 900 });
13498
+ } else {
13499
+ setTimeout(function() { captureLiquidBackdropFrame({ maxAge: 5000 }); }, 600);
13500
+ }
13501
+
13502
+ document.addEventListener('pointerover', function(event) {
13503
+ var target = event.target && event.target.closest ? event.target.closest('.mode-toggle-btn[data-mode], .left-nav-icon, .lt-item, .left-ws-item, .tab-btn[data-tab], .model-select-menu-option, .newmark-select-option, .conv-item') : null;
13504
+ if (target) scheduleLiquidBackdropPrime();
13505
+ }, { passive: true });
13506
+ document.addEventListener('focusin', function(event) {
13507
+ var target = event.target && event.target.closest ? event.target.closest('.mode-toggle-btn[data-mode], .left-nav-icon, .lt-item, .left-ws-item, .tab-btn[data-tab], .model-select-menu-option, .newmark-select-option, .conv-item') : null;
13508
+ if (target) scheduleLiquidBackdropPrime();
13509
+ });
13510
+
13511
+ function compileLiquidShader(gl, type, source) {
13512
+ var shader = gl.createShader(type);
13513
+ gl.shaderSource(shader, source);
13514
+ gl.compileShader(shader);
13515
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
13516
+ console.error('[KyantGlass] shader compile failed:', gl.getShaderInfoLog(shader));
13517
+ gl.deleteShader(shader);
13518
+ return null;
13519
+ }
13520
+ return shader;
13521
+ }
13522
+
13523
+ function liquidGlassCssNumber(name, fallback) {
13524
+ var value = parseFloat(getComputedStyle(document.documentElement).getPropertyValue(name));
13525
+ return Number.isFinite(value) ? value : fallback;
13526
+ }
13527
+
13528
+ var liquidKyantRendererPool = [];
13529
+ var liquidKyantVertexSource = '#version 300 es\n' +
13530
+ 'in vec2 p; out vec2 uv; void main(){uv=p*.5+.5;gl_Position=vec4(p,0.,1.);}';
13531
+ var liquidKyantFragmentSource = '#version 300 es\nprecision highp float;\n' +
13532
+ 'uniform sampler2D content; uniform vec2 size; uniform vec2 viewport; uniform vec2 origin; uniform vec4 cornerRadii; uniform float refractionHeight; uniform float refractionAmount; uniform float depthEffect; uniform float chromaticAberration; in vec2 uv; out vec4 outColor;\n' +
13533
+ 'float radiusAt(vec2 coord,vec4 radii){if(coord.x>=0.)return coord.y<=0.?radii.y:radii.z;return coord.y<=0.?radii.x:radii.w;}\n' +
13534
+ 'float sdRoundedRect(vec2 coord,vec2 halfSize,float r){vec2 q=abs(coord)-(halfSize-vec2(r));return length(max(q,0.))-r+min(max(q.x,q.y),0.);}\n' +
13535
+ 'vec2 gradSdRoundedRect(vec2 coord,vec2 halfSize,float r){vec2 q=abs(coord)-(halfSize-vec2(r));if(q.x>=0.||q.y>=0.)return sign(coord)*normalize(max(q,vec2(0.0001)));float gx=step(q.y,q.x);return sign(coord)*vec2(gx,1.-gx);}\n' +
13536
+ 'float circleMap(float x){x=clamp(x,0.,1.);return 1.-sqrt(max(0.,1.-x*x));}\n' +
13537
+ 'vec4 sampleBackdrop(vec2 cssCoord){vec2 t=vec2(cssCoord.x/viewport.x,1.-cssCoord.y/viewport.y);return texture(content,clamp(t,vec2(0.),vec2(1.)));}\n' +
13538
+ 'void main(){vec2 local=vec2(uv.x*size.x,(1.-uv.y)*size.y);vec2 halfSize=size*.5;vec2 centered=local-halfSize;float radius=radiusAt(centered,cornerRadii);float sd=sdRoundedRect(centered,halfSize,radius);if(sd>0.)discard;vec2 base=origin+local;if(-sd>=refractionHeight){outColor=vec4(0.);return;}sd=min(sd,0.);float edge=clamp(1.-(-sd/refractionHeight),0.,1.);float d=circleMap(edge)*refractionAmount;float gradRadius=min(radius*1.5,min(halfSize.x,halfSize.y));vec2 grad=normalize(gradSdRoundedRect(centered,halfSize,gradRadius)+depthEffect*normalize(centered));vec2 refracted=base+d*grad;float dispersionIntensity=chromaticAberration*edge;vec2 dispersed=d*grad*dispersionIntensity;vec4 red=sampleBackdrop(refracted+dispersed);vec4 green=sampleBackdrop(refracted);vec4 blue=sampleBackdrop(refracted-dispersed);float alpha=mix(.14,.82,edge);outColor=vec4(vec3(red.r,green.g,blue.b)*alpha,alpha);}';
13539
+
13540
+ function createKyantLiquidRenderer() {
13541
+ var canvas = document.createElement('canvas');
13542
+ var displayCanvas = document.createElement('canvas');
13543
+ displayCanvas.className = 'liquid-selection-canvas';
13544
+ var displayContext = displayCanvas.getContext('2d', { alpha:true });
13545
+ if (!displayContext) return null;
13546
+ var gl = canvas.getContext('webgl2', { alpha:true, antialias:false, premultipliedAlpha:true, powerPreference:'high-performance' });
13547
+ if (!gl) return null;
13548
+ var vs = compileLiquidShader(gl, gl.VERTEX_SHADER, liquidKyantVertexSource);
13549
+ var fs = compileLiquidShader(gl, gl.FRAGMENT_SHADER, liquidKyantFragmentSource);
13550
+ if (!vs || !fs) return null;
13551
+ var program = gl.createProgram();
13552
+ gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program);
13553
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
13554
+ console.error('[KyantGlass] program link failed:', gl.getProgramInfoLog(program));
13555
+ return null;
13556
+ }
13557
+ gl.deleteShader(vs);
13558
+ gl.deleteShader(fs);
13559
+ var buffer = gl.createBuffer();
13560
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
13561
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]), gl.STATIC_DRAW);
13562
+ var location = gl.getAttribLocation(program, 'p');
13563
+ gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location,2,gl.FLOAT,false,0,0);
13564
+ var texture = gl.createTexture();
13565
+ gl.bindTexture(gl.TEXTURE_2D, texture);
13566
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
13567
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
13568
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
13569
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
13570
+ var uniforms = {
13571
+ content: gl.getUniformLocation(program,'content'),
13572
+ size: gl.getUniformLocation(program,'size'),
13573
+ viewport: gl.getUniformLocation(program,'viewport'),
13574
+ origin: gl.getUniformLocation(program,'origin'),
13575
+ cornerRadii: gl.getUniformLocation(program,'cornerRadii'),
13576
+ refractionHeight: gl.getUniformLocation(program,'refractionHeight'),
13577
+ refractionAmount: gl.getUniformLocation(program,'refractionAmount'),
13578
+ depthEffect: gl.getUniformLocation(program,'depthEffect'),
13579
+ chromaticAberration: gl.getUniformLocation(program,'chromaticAberration')
13580
+ };
13581
+ gl.useProgram(program);
13582
+ gl.uniform1i(uniforms.content, 0);
13583
+ var renderer = {
13584
+ canvas: canvas,
13585
+ displayCanvas: displayCanvas,
13586
+ displayContext: displayContext,
13587
+ gl: gl,
13588
+ program: program,
13589
+ texture: texture,
13590
+ uniforms: uniforms,
13591
+ owner: null,
13592
+ textureReady: false,
13593
+ generation: 0,
13594
+ renderFrame: 0,
13595
+ frameId: '',
13596
+ lastGeometry: null,
13597
+ pendingGeometry: null,
13598
+ uniformState: Object.create(null),
13599
+ invalid: false
13600
+ };
13601
+ canvas.addEventListener('webglcontextlost', function(event) {
13602
+ event.preventDefault();
13603
+ renderer.invalid = true;
13604
+ renderer.textureReady = false;
13605
+ if (renderer.renderFrame) cancelAnimationFrame(renderer.renderFrame);
13606
+ renderer.renderFrame = 0;
13607
+ liquidKyantRendererPool = liquidKyantRendererPool.filter(function(item) { return item !== renderer; });
13608
+ });
13609
+ return renderer;
13610
+ }
13611
+
13612
+ function acquireKyantLiquidRenderer(float) {
13613
+ var renderer = liquidKyantRendererPool.find(function(item) {
13614
+ return !item.invalid && (!item.owner || !item.owner.isConnected);
13615
+ });
13616
+ if (!renderer) {
13617
+ renderer = createKyantLiquidRenderer();
13618
+ if (!renderer) return null;
13619
+ liquidKyantRendererPool.push(renderer);
13620
+ }
13621
+ renderer.owner = float;
13622
+ renderer.generation += 1;
13623
+ if (renderer.renderFrame) cancelAnimationFrame(renderer.renderFrame);
13624
+ renderer.renderFrame = 0;
13625
+ renderer.displayCanvas.remove();
13626
+ renderer.lastGeometry = null;
13627
+ renderer.pendingGeometry = null;
13628
+ return renderer;
13629
+ }
13630
+
13631
+ function uploadKyantLiquidFrame(renderer, frame) {
13632
+ if (!renderer || !frame || !frame.bitmap || renderer.frameId === frame.id) return false;
13633
+ var gl = renderer.gl;
13634
+ gl.bindTexture(gl.TEXTURE_2D, renderer.texture);
13635
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
13636
+ gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,frame.bitmap);
13637
+ renderer.textureReady = true;
13638
+ renderer.frameId = frame.id;
13639
+ return true;
13640
+ }
13641
+
13642
+ function prewarmKyantRenderer(frame) {
13643
+ if (!frame || !frame.bitmap) return;
13644
+ var renderer = liquidKyantRendererPool.find(function(item) { return !item.invalid && !item.owner; });
13645
+ if (!renderer) {
13646
+ if (liquidKyantRendererPool.some(function(item) { return !item.invalid; })) return;
13647
+ renderer = createKyantLiquidRenderer();
13648
+ if (!renderer) return;
13649
+ liquidKyantRendererPool.push(renderer);
13650
+ }
13651
+ uploadKyantLiquidFrame(renderer, frame);
13652
+ }
13653
+
13654
+ function attachKyantLiquidRenderer(float, frame) {
13655
+ var renderer = float._kyantRendererRef;
13656
+ if (!renderer || renderer.owner !== float) renderer = acquireKyantLiquidRenderer(float);
13657
+ if (!renderer) { float.dataset.kyantRenderer = 'webgl2-unavailable'; return; }
13658
+ float._kyantRendererRef = renderer;
13659
+ var canvas = renderer.canvas;
13660
+ var displayCanvas = renderer.displayCanvas;
13661
+ var displayContext = renderer.displayContext;
13662
+ var gl = renderer.gl;
13663
+ var program = renderer.program;
13664
+ var texture = renderer.texture;
13665
+ var uniforms = renderer.uniforms;
13666
+ var generation = renderer.generation;
13667
+ float.dataset.kyantRenderer = 'webgl2';
13668
+ float._kyantTextureReady = false;
13669
+ float._kyantDrawReady = false;
13670
+ delete float.dataset.kyantDrawReady;
13671
+ float._renderKyantGlass = function(geometry, immediate) {
13672
+ renderer.pendingGeometry = geometry || null;
13673
+ if (!renderer.textureReady || renderer.owner !== float || !float.isConnected) return;
13674
+ if (immediate && renderer.renderFrame) {
13675
+ cancelAnimationFrame(renderer.renderFrame);
13676
+ renderer.renderFrame = 0;
13677
+ }
13678
+ if (renderer.renderFrame) return;
13679
+ var draw = function() {
13680
+ renderer.renderFrame = 0;
13681
+ if (!renderer.textureReady || renderer.owner !== float || !float.isConnected) return;
13682
+ var geometry = renderer.pendingGeometry;
13683
+ renderer.pendingGeometry = null;
13684
+ var style = geometry ? null : getComputedStyle(float);
13685
+ var width = geometry ? geometry.width : (parseFloat(style.width) || float.offsetWidth || 1);
13686
+ var height = geometry ? geometry.height : (parseFloat(style.height) || float.offsetHeight || 1);
13687
+ var left = geometry ? geometry.left : (parseFloat(style.left) || 0);
13688
+ var top = geometry ? geometry.top : (parseFloat(style.top) || 0);
13689
+ var dpr = window.devicePixelRatio || 1;
13690
+ var renderScale = Math.min(dpr, 1.5);
13691
+ var pixelWidth = Math.max(1, Math.round(width*renderScale));
13692
+ var pixelHeight = Math.max(1, Math.round(height*renderScale));
13693
+ var geometryKey = [left.toFixed(2),top.toFixed(2),width.toFixed(2),height.toFixed(2),pixelWidth,pixelHeight].join(':');
13694
+ if (renderer.lastGeometry === geometryKey) return;
13695
+ renderer.lastGeometry = geometryKey;
13696
+ if (canvas.width !== pixelWidth) canvas.width = pixelWidth;
13697
+ if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
13698
+ if (displayCanvas.width !== pixelWidth) displayCanvas.width = pixelWidth;
13699
+ if (displayCanvas.height !== pixelHeight) displayCanvas.height = pixelHeight;
13700
+ gl.viewport(0,0,pixelWidth,pixelHeight);
13701
+ gl.useProgram(program);
13702
+ function uniformChanged(key, values, apply) {
13703
+ var prior = renderer.uniformState[key];
13704
+ if (prior && prior.length === values.length && values.every(function(value,index) { return prior[index] === value; })) return;
13705
+ renderer.uniformState[key] = values;
13706
+ apply();
13707
+ }
13708
+ uniformChanged('size',[width,height],function() { gl.uniform2f(uniforms.size,width,height); });
13709
+ uniformChanged('viewport',[window.innerWidth,window.innerHeight],function() { gl.uniform2f(uniforms.viewport,window.innerWidth,window.innerHeight); });
13710
+ uniformChanged('origin',[left,top],function() { gl.uniform2f(uniforms.origin,left,top); });
13711
+ var capsuleRadius = Math.min(width,height)*.5;
13712
+ uniformChanged('cornerRadii',[capsuleRadius],function() { gl.uniform4f(uniforms.cornerRadii,capsuleRadius,capsuleRadius,capsuleRadius,capsuleRadius); });
13713
+ var configuredHeight = liquidGlassCssNumber('--liquid-interaction-edge', 6);
13714
+ var configuredAmount = liquidGlassCssNumber('--liquid-float-refraction-amount', 24);
13715
+ var configuredChromatic = liquidGlassCssNumber('--liquid-float-chromatic-aberration', 1);
13716
+ var refractionHeight = Math.max(0.001,Math.min(configuredHeight,height*.48));
13717
+ uniformChanged('refractionHeight',[refractionHeight],function() { gl.uniform1f(uniforms.refractionHeight,refractionHeight); });
13718
+ uniformChanged('refractionAmount',[-configuredAmount],function() { gl.uniform1f(uniforms.refractionAmount,-configuredAmount); });
13719
+ uniformChanged('depthEffect',[1],function() { gl.uniform1f(uniforms.depthEffect,1); });
13720
+ uniformChanged('chromaticAberration',[configuredChromatic],function() { gl.uniform1f(uniforms.chromaticAberration,configuredChromatic); });
13721
+ gl.drawArrays(gl.TRIANGLES,0,6);
13722
+ displayContext.clearRect(0,0,pixelWidth,pixelHeight);
13723
+ displayContext.drawImage(canvas,0,0);
13724
+ float._kyantDrawReady = true;
13725
+ float.dataset.kyantDrawReady = 'true';
13726
+ if (displayCanvas.parentNode !== float) float.appendChild(displayCanvas);
13727
+ };
13728
+ if (immediate) draw();
13729
+ else renderer.renderFrame = requestAnimationFrame(draw);
13730
+ };
13731
+ float._releaseKyantGlass = function() {
13732
+ if (renderer.owner !== float) return;
13733
+ renderer.generation += 1;
13734
+ renderer.owner = null;
13735
+ if (renderer.renderFrame) cancelAnimationFrame(renderer.renderFrame);
13736
+ renderer.renderFrame = 0;
13737
+ renderer.lastGeometry = null;
13738
+ renderer.pendingGeometry = null;
13739
+ displayCanvas.remove();
13740
+ delete float._kyantRendererRef;
13741
+ };
13742
+ if (frame && frame.bitmap && renderer.owner === float && renderer.generation === generation && float.isConnected) {
13743
+ uploadKyantLiquidFrame(renderer, frame);
13744
+ float._kyantTextureReady = true;
13745
+ if (float.style.width && float.style.height) float._renderKyantGlass(null, true);
13746
+ } else if (renderer.textureReady) {
13747
+ float._kyantTextureReady = true;
13748
+ if (float.style.width && float.style.height) float._renderKyantGlass(null, true);
13749
+ } else {
13750
+ float.dataset.kyantRenderer = 'waiting-backdrop';
13751
+ }
13752
+ }
13753
+
13754
+ function mountLiquidViewportFloat(float) {
13755
+ if (liquidBackdropPrimeTimer) clearTimeout(liquidBackdropPrimeTimer);
13756
+ liquidBackdropPrimeTimer = 0;
13757
+ float.classList.add('liquid-selection-viewport');
13758
+ float.setAttribute('popover', 'manual');
13759
+ document.body.appendChild(float);
13760
+ startLiquidMotionTracking(float);
13761
+ attachKyantLiquidRenderer(float, liquidBackdropCachedFrame);
13762
+ }
13763
+
13764
+ function showLiquidViewportFloat(float) {
13765
+ try { float.showPopover(); } catch (_) {}
13766
+ }
13767
+
13768
+ function keepLiquidFloatExpansionInsideViewport(float, rect) {
13769
+ if (!float || !rect) return;
13770
+ var edge = 28;
13771
+ float.style.transformOrigin = rect.left < edge ? 'left center' : (rect.right > window.innerWidth - edge ? 'right center' : 'center');
13772
+ }
13773
+
13774
+ function liquidInteractionEdgePx() {
13775
+ return liquidGlassCssNumber('--liquid-interaction-edge', 6);
13776
+ }
13777
+
13778
+ function startLiquidMotionTracking(float) {
13779
+ if (!float || float._liquidMotionFrame) return;
13780
+ var lastTime = 0;
13781
+ var lastX = 0;
13782
+ var lastY = 0;
13783
+ var velocityX = 0;
13784
+ var velocityY = 0;
13785
+ var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13786
+ function frame(now) {
13787
+ if (!float || !float.isConnected) {
13788
+ if (float) float._liquidMotionFrame = 0;
13789
+ return;
13790
+ }
13791
+ var rect = float.getBoundingClientRect();
13792
+ var left = rect.left;
13793
+ var top = rect.top;
13794
+ var width = rect.width;
13795
+ var height = rect.height;
13796
+ var centerX = left + width * .5;
13797
+ var centerY = top + height * .5;
13798
+ var moved = lastTime > 0 && (Math.abs(centerX - lastX) > .01 || Math.abs(centerY - lastY) > .01);
13799
+ if (lastTime > 0) {
13800
+ var seconds = Math.max(.001, Math.min(.05, (now - lastTime) / 1000));
13801
+ var instantaneousX = (centerX - lastX) / seconds;
13802
+ var instantaneousY = (centerY - lastY) / seconds;
13803
+ velocityX = velocityX * .45 + instantaneousX * .55;
13804
+ velocityY = velocityY * .45 + instantaneousY * .55;
13805
+ }
13806
+ lastTime = now;
13807
+ lastX = centerX;
13808
+ lastY = centerY;
13809
+ var speed = reducedMotion ? 0 : Math.hypot(velocityX, velocityY);
13810
+ var amount = Math.max(0, Math.min(1, (speed - 8) / 700));
13811
+ var angle = speed > 1 ? Math.atan2(velocityY, velocityX) : 0;
13812
+ float.style.setProperty('--liquid-motion-angle', angle + 'rad');
13813
+ float.style.setProperty('--liquid-motion-stretch', String(1 + amount * .075));
13814
+ float.style.setProperty('--liquid-motion-squash', String(1 - amount * .035));
13815
+ if (window.__liquidPerfCounts) float.dataset.liquidMotionAmount = amount.toFixed(4);
13816
+ if (moved && float._renderKyantGlass) float._renderKyantGlass({ left:left, top:top, width:width, height:height }, true);
13817
+ float._liquidMotionFrame = requestAnimationFrame(frame);
13818
+ }
13819
+ float._liquidMotionFrame = requestAnimationFrame(frame);
13820
+ }
13821
+
13822
+ function stopLiquidMotionTracking(float) {
13823
+ if (!float) return;
13824
+ if (float._liquidArrivalFrame) cancelAnimationFrame(float._liquidArrivalFrame);
13825
+ float._liquidArrivalFrame = 0;
13826
+ if (float._liquidMotionFrame) cancelAnimationFrame(float._liquidMotionFrame);
13827
+ float._liquidMotionFrame = 0;
13828
+ float.style.removeProperty('--liquid-motion-angle');
13829
+ float.style.removeProperty('--liquid-motion-stretch');
13830
+ float.style.removeProperty('--liquid-motion-squash');
13831
+ }
13832
+
13833
+ function waitForLiquidSelectionArrival(float, onArrive) {
13834
+ if (!float || !float.isConnected) { onArrive(); return; }
13835
+ var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13836
+ if (reducedMotion) { onArrive(); return; }
13837
+ if (float._liquidArrivalFrame) cancelAnimationFrame(float._liquidArrivalFrame);
13838
+ var startedAt = performance.now();
13839
+ function settled() {
13840
+ if (!float || !float.isConnected) return true;
13841
+ var target = float._liquidTargetRect;
13842
+ if (!target) return true;
13843
+ var style = getComputedStyle(float);
13844
+ var left = parseFloat(style.left) || 0;
13845
+ var top = parseFloat(style.top) || 0;
13846
+ var width = parseFloat(style.width) || 0;
13847
+ var height = parseFloat(style.height) || 0;
13848
+ return Math.abs(left - target.left) < .75 && Math.abs(top - target.top) < .75 &&
13849
+ Math.abs(width - target.width) < .75 && Math.abs(height - target.height) < .75 &&
13850
+ parseFloat(style.opacity || '0') >= .995;
13851
+ }
13852
+ function inspect() {
13853
+ if (settled() || performance.now() - startedAt > 300) {
13854
+ float._liquidArrivalFrame = 0;
13855
+ onArrive();
13856
+ return;
13857
+ }
13858
+ float._liquidArrivalFrame = requestAnimationFrame(inspect);
13859
+ }
13860
+ float._liquidArrivalFrame = requestAnimationFrame(inspect);
13861
+ }
13862
+
13863
+ function landLiquidSelectionFloat(float, cleanup, onComplete) {
13864
+ if (!float) { cleanup(); if (onComplete) onComplete(); return; }
13865
+ var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13866
+ if (!float.isConnected) { cleanup(); if (onComplete) onComplete(); return; }
13867
+ stopLiquidMotionTracking(float);
13868
+ float.classList.add('liquid-selection-landing');
13869
+ float._liquidLandingTimer = setTimeout(function() {
13870
+ cleanup();
13871
+ scheduleLiquidBackdropPrime();
13872
+ if (onComplete) onComplete();
13873
+ }, reducedMotion ? 0 : 120);
13874
+ }
13875
+
13876
+ function wireLiquidMenuInteractions(menu) {
13877
+ if (!menu || menu.dataset.liquidInteractions === 'true') return;
13878
+ menu.dataset.liquidInteractions = 'true';
13879
+ var pointer = null;
13880
+ var source = null;
13881
+ var target = null;
13882
+ var float = null;
13883
+ var generation = 0;
13884
+ var pressX = 0;
13885
+ var pressY = 0;
13886
+ var gestureMoved = false;
13887
+ function optionAt(event) {
13888
+ var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
13889
+ var node = hit && hit.closest ? hit.closest('button.model-select-menu-option, button.newmark-select-option') : null;
13890
+ return node && menu.contains(node) && !node.disabled ? node : null;
13891
+ }
13892
+ function position(option) {
13893
+ if (!option || !float) return;
13894
+ if (float.classList.contains('liquid-selection-viewport')) {
13895
+ var rect = option.getBoundingClientRect();
13896
+ var edge = liquidInteractionEdgePx();
13897
+ keepLiquidFloatExpansionInsideViewport(float, rect);
13898
+ float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
13899
+ float.style.left = (rect.left - edge) + 'px';
13900
+ float.style.top = (rect.top - edge) + 'px';
13901
+ float.style.width = (rect.width + edge * 2) + 'px';
13902
+ float.style.height = (rect.height + edge * 2) + 'px';
13903
+ if (float._renderKyantGlass) float._renderKyantGlass({
13904
+ left: rect.left - edge, top: rect.top - edge,
13905
+ width: rect.width + edge * 2, height: rect.height + edge * 2
13906
+ }, true);
13907
+ return;
13908
+ }
13909
+ var edge = liquidInteractionEdgePx();
13910
+ float.style.left = (option.offsetLeft - edge) + 'px';
13911
+ float.style.top = (option.offsetTop - edge) + 'px';
13912
+ float.style.width = (option.offsetWidth + edge * 2) + 'px';
13913
+ float.style.height = (option.offsetHeight + edge * 2) + 'px';
13914
+ }
13915
+ function positionAlongPointer(event) {
13916
+ if (!float || !event) return;
13917
+ var menuRect = menu.getBoundingClientRect();
13918
+ var clipTop = Math.max(0, menuRect.top + menu.clientTop + 7);
13919
+ var clipBottom = Math.min(window.innerHeight, menuRect.bottom - menu.clientTop - 7);
13920
+ var allOptions = Array.from(menu.querySelectorAll('button.model-select-menu-option, button.newmark-select-option'))
13921
+ .filter(function(option) { return !option.disabled && option.getClientRects().length; });
13922
+ var options = allOptions.filter(function(option) {
13923
+ var rect = option.getBoundingClientRect();
13924
+ return rect.bottom > clipTop && rect.top < clipBottom;
13925
+ });
13926
+ if (!options.length) return;
13927
+ var nearest = options[0];
13928
+ var nearestDistance = Infinity;
13929
+ options.forEach(function(option) {
13930
+ var rect = option.getBoundingClientRect();
13931
+ var distance = Math.abs(event.clientY - (rect.top + rect.height / 2));
13932
+ if (distance < nearestDistance) { nearest = option; nearestDistance = distance; }
13933
+ });
13934
+ target = nearest;
13935
+ var rect = nearest.getBoundingClientRect();
13936
+ var firstRect = options[0].getBoundingClientRect();
13937
+ var lastRect = options[options.length - 1].getBoundingClientRect();
13938
+ var minCenter = Math.min(firstRect.top + firstRect.height / 2, lastRect.top + lastRect.height / 2);
13939
+ var maxCenter = Math.max(firstRect.top + firstRect.height / 2, lastRect.top + lastRect.height / 2);
13940
+ if (maxCenter < minCenter) maxCenter = minCenter;
13941
+ var centerY = Math.max(minCenter, Math.min(maxCenter, event.clientY));
13942
+ var edge = liquidInteractionEdgePx();
13943
+ float.style.left = (rect.left - edge) + 'px';
13944
+ float.style.top = (centerY - rect.height / 2 - edge) + 'px';
13945
+ float.style.width = (rect.width + edge * 2) + 'px';
13946
+ float.style.height = (rect.height + edge * 2) + 'px';
13947
+ }
13948
+ function detachWindowTracking() {
13949
+ window.removeEventListener('pointermove', onPointerMove, true);
13950
+ window.removeEventListener('pointerup', onPointerUp, true);
13951
+ window.removeEventListener('pointercancel', onPointerCancel, true);
13952
+ }
13953
+ function clear() {
13954
+ detachWindowTracking();
13955
+ if (source) source.classList.remove('liquid-selection-source');
13956
+ stopLiquidMotionTracking(float);
13957
+ if (float && float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
13958
+ if (float && float._releaseKyantGlass) float._releaseKyantGlass();
13959
+ if (float && float.parentNode) float.parentNode.removeChild(float);
13960
+ generation += 1;
13961
+ pointer = source = target = float = null;
13962
+ }
13963
+ function onPointerMove(event) {
13964
+ if (pointer === null || event.pointerId !== pointer) return;
13965
+ if (!gestureMoved && Math.hypot(event.clientX - pressX, event.clientY - pressY) >= 4) {
13966
+ gestureMoved = true;
13967
+ }
13968
+ event.preventDefault();
13969
+ positionAlongPointer(event);
13970
+ }
13971
+ function onPointerUp(event) {
13972
+ if (pointer === null || event.pointerId !== pointer) return;
13973
+ var commit = target;
13974
+ detachWindowTracking();
13975
+ try { menu.releasePointerCapture(pointer); } catch (_) {}
13976
+ pointer = null;
13977
+ if (commit) position(commit);
13978
+ waitForLiquidSelectionArrival(float, function() {
13979
+ landLiquidSelectionFloat(float, clear, function() {
13980
+ if (commit && commit.isConnected) commit.click();
13981
+ });
13982
+ });
13983
+ }
13984
+ function onPointerCancel(event) {
13985
+ if (pointer === null || event.pointerId === pointer) clear();
13986
+ }
13987
+ menu.addEventListener('pointerdown', function(event) {
13988
+ var pressed = optionAt(event);
13989
+ if (!pressed || event.button !== 0) return;
13990
+ event.preventDefault();
13991
+ target = pressed;
13992
+ pointer = event.pointerId;
13993
+ pressX = event.clientX;
13994
+ pressY = event.clientY;
13995
+ gestureMoved = false;
13996
+ var captureGeneration = ++generation;
13997
+ menu.setPointerCapture(pointer);
13998
+ window.addEventListener('pointermove', onPointerMove, true);
13999
+ window.addEventListener('pointerup', onPointerUp, true);
14000
+ window.addEventListener('pointercancel', onPointerCancel, true);
14001
+ if (float && float.isConnected) {
14002
+ if (float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
14003
+ float._liquidLandingTimer = 0;
14004
+ float.classList.remove('liquid-selection-landing');
14005
+ float.classList.add('visible');
14006
+ position(target);
14007
+ } else {
14008
+ var selected = menu.querySelector('button.model-select-menu-option.selected, button.newmark-select-option.selected');
14009
+ source = selected || pressed;
14010
+ source.classList.add('liquid-selection-source');
14011
+ float = document.createElement('div');
14012
+ float.className = 'liquid-selection-float liquid-selection-primed';
14013
+ mountLiquidViewportFloat(float);
14014
+ position(source);
14015
+ showLiquidViewportFloat(float);
14016
+ position(source);
14017
+ float.getBoundingClientRect();
14018
+ requestAnimationFrame(function() {
14019
+ if (!float) return;
14020
+ float.classList.remove('liquid-selection-primed');
14021
+ float.classList.add('visible');
14022
+ requestAnimationFrame(function() { if (float) position(target); });
14023
+ });
14024
+ }
14025
+ captureLiquidBackdropFrame().then(function(backdropFrame) {
14026
+ if (generation === captureGeneration && float && float.isConnected) attachKyantLiquidRenderer(float, backdropFrame);
14027
+ }).catch(function() {});
14028
+ });
14029
+ menu.addEventListener('click', function(event) {
14030
+ if (!event.isTrusted || !float || !float.isConnected) return;
14031
+ event.preventDefault();
14032
+ event.stopImmediatePropagation();
14033
+ }, true);
14034
+ }
14035
+
14036
+ // The same single floating capsule is used by the left workspace rail and
14037
+ // right sidebar tabs. Delegation keeps it valid while either rail rerenders.
14038
+ function wireLiquidRailInteractions(container, selector) {
14039
+ if (!container || container.dataset.liquidRailInteractions === 'true') return;
14040
+ container.dataset.liquidRailInteractions = 'true';
14041
+ var pointer = null;
14042
+ var source = null;
14043
+ var target = null;
14044
+ var float = null;
14045
+ var generation = 0;
14046
+ var pressX = 0;
14047
+ var pressY = 0;
14048
+ var gestureMoved = false;
14049
+ function itemAt(event) {
14050
+ var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
14051
+ var node = hit && hit.closest ? hit.closest(selector) : null;
14052
+ return node && container.contains(node) && !node.disabled ? node : null;
14053
+ }
14054
+ function position(item) {
14055
+ if (!item || !float) return;
14056
+ if (float.classList.contains('liquid-selection-viewport')) {
14057
+ var rect = item.getBoundingClientRect();
14058
+ var edge = liquidInteractionEdgePx();
14059
+ keepLiquidFloatExpansionInsideViewport(float, rect);
14060
+ float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
14061
+ float.style.left = (rect.left - edge) + 'px';
14062
+ float.style.top = (rect.top - edge) + 'px';
14063
+ float.style.width = (rect.width + edge * 2) + 'px';
14064
+ float.style.height = (rect.height + edge * 2) + 'px';
14065
+ if (float._renderKyantGlass) float._renderKyantGlass({
14066
+ left: rect.left - edge, top: rect.top - edge,
14067
+ width: rect.width + edge * 2, height: rect.height + edge * 2
14068
+ }, true);
14069
+ return;
14070
+ }
14071
+ var edge = liquidInteractionEdgePx();
14072
+ float.style.left = (item.offsetLeft - edge) + 'px';
14073
+ float.style.top = (item.offsetTop - edge) + 'px';
14074
+ float.style.width = (item.offsetWidth + edge * 2) + 'px';
14075
+ float.style.height = (item.offsetHeight + edge * 2) + 'px';
14076
+ }
14077
+ function positionAlongPointer(event) {
14078
+ if (!float || !event) return;
14079
+ var items = Array.from(container.querySelectorAll(selector))
14080
+ .filter(function(item) { return !item.disabled && item.getClientRects().length; });
14081
+ if (!items.length) return;
14082
+ var rects = items.map(function(item) { return item.getBoundingClientRect(); });
14083
+ var centersX = rects.map(function(rect) { return rect.left + rect.width / 2; });
14084
+ var centersY = rects.map(function(rect) { return rect.top + rect.height / 2; });
14085
+ var horizontal = Math.max.apply(null, centersX) - Math.min.apply(null, centersX) >
14086
+ Math.max.apply(null, centersY) - Math.min.apply(null, centersY);
14087
+ var pointerAxis = horizontal ? event.clientX : event.clientY;
14088
+ var centers = horizontal ? centersX : centersY;
14089
+ var nearestIndex = 0;
14090
+ var nearestDistance = Infinity;
14091
+ centers.forEach(function(center, index) {
14092
+ var distance = Math.abs(pointerAxis - center);
14093
+ if (distance < nearestDistance) { nearestIndex = index; nearestDistance = distance; }
14094
+ });
14095
+ target = items[nearestIndex];
14096
+ var rect = rects[nearestIndex];
14097
+ var minCenter = Math.min.apply(null, centers);
14098
+ var maxCenter = Math.max.apply(null, centers);
14099
+ var projectedCenter = Math.max(minCenter, Math.min(maxCenter, pointerAxis));
14100
+ var edge = liquidInteractionEdgePx();
14101
+ var left = horizontal ? projectedCenter - rect.width / 2 - edge : rect.left - edge;
14102
+ var top = horizontal ? rect.top - edge : projectedCenter - rect.height / 2 - edge;
14103
+ float.style.left = left + 'px';
14104
+ float.style.top = top + 'px';
14105
+ float.style.width = (rect.width + edge * 2) + 'px';
14106
+ float.style.height = (rect.height + edge * 2) + 'px';
14107
+ }
14108
+ function detachWindowTracking() {
14109
+ window.removeEventListener('pointermove', onPointerMove, true);
14110
+ window.removeEventListener('pointerup', onPointerUp, true);
14111
+ window.removeEventListener('pointercancel', onPointerCancel, true);
14112
+ }
14113
+ function clear() {
14114
+ detachWindowTracking();
14115
+ if (source) source.classList.remove('liquid-selection-source');
14116
+ stopLiquidMotionTracking(float);
14117
+ if (float && float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
14118
+ if (float && float._releaseKyantGlass) float._releaseKyantGlass();
14119
+ if (float && float.parentNode) float.parentNode.removeChild(float);
14120
+ generation += 1;
14121
+ pointer = source = target = float = null;
14122
+ window.setLiquidSidebarGestureLock('rail:' + (container.id || selector), false);
14123
+ }
14124
+ function onPointerMove(event) {
14125
+ if (pointer === null || event.pointerId !== pointer) return;
14126
+ if (!gestureMoved && Math.hypot(event.clientX - pressX, event.clientY - pressY) >= 4) {
14127
+ gestureMoved = true;
14128
+ }
14129
+ event.preventDefault();
14130
+ positionAlongPointer(event);
14131
+ }
14132
+ function onPointerUp(event) {
14133
+ if (pointer === null || event.pointerId !== pointer) return;
14134
+ var commit = target;
14135
+ detachWindowTracking();
14136
+ try { container.releasePointerCapture(pointer); } catch (_) {}
14137
+ pointer = null;
14138
+ if (commit) {
14139
+ position(commit);
14140
+ }
14141
+ waitForLiquidSelectionArrival(float, function() {
14142
+ landLiquidSelectionFloat(float, clear, function() {
14143
+ if (!commit || !commit.isConnected) return;
14144
+ if (container.id === 'left-tool-surface' || container.id === 'left-thumb') {
14145
+ container.querySelectorAll('.liquid-tool-active').forEach(function(item) { item.classList.remove('liquid-tool-active'); });
14146
+ commit.classList.add('liquid-tool-active');
14147
+ }
14148
+ commit.click();
14149
+ });
14150
+ });
14151
+ }
14152
+ function onPointerCancel(event) {
14153
+ if (pointer === null || event.pointerId === pointer) clear();
14154
+ }
14155
+ container.addEventListener('pointerdown', function(event) {
14156
+ if (window.liquidSidebarGesturesLocked() && !(float && float.isConnected)) return;
14157
+ var pressed = itemAt(event);
14158
+ if (!pressed || event.button !== 0) return;
14159
+ event.preventDefault();
14160
+ target = pressed;
14161
+ pointer = event.pointerId;
14162
+ pressX = event.clientX;
14163
+ pressY = event.clientY;
14164
+ gestureMoved = false;
14165
+ var captureGeneration = ++generation;
14166
+ window.setLiquidSidebarGestureLock('rail:' + (container.id || selector), true);
14167
+ container.setPointerCapture(pointer);
14168
+ window.addEventListener('pointermove', onPointerMove, true);
14169
+ window.addEventListener('pointerup', onPointerUp, true);
14170
+ window.addEventListener('pointercancel', onPointerCancel, true);
14171
+ if (float && float.isConnected) {
14172
+ if (float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
14173
+ float._liquidLandingTimer = 0;
14174
+ float.classList.remove('liquid-selection-landing');
14175
+ float.classList.add('visible');
14176
+ position(target);
14177
+ } else {
14178
+ source = container.querySelector('.liquid-tool-active') || container.querySelector(selector + '.active') || pressed;
14179
+ source.classList.add('liquid-selection-source');
14180
+ float = document.createElement('div');
14181
+ float.className = 'liquid-selection-float liquid-selection-primed';
14182
+ mountLiquidViewportFloat(float);
14183
+ position(source);
14184
+ showLiquidViewportFloat(float);
14185
+ position(source);
14186
+ float.getBoundingClientRect();
14187
+ requestAnimationFrame(function() {
14188
+ if (!float) return;
14189
+ float.classList.remove('liquid-selection-primed');
14190
+ float.classList.add('visible');
14191
+ requestAnimationFrame(function() { if (float) position(target); });
14192
+ });
14193
+ }
14194
+ captureLiquidBackdropFrame().then(function(backdropFrame) {
14195
+ if (generation === captureGeneration && float && float.isConnected) attachKyantLiquidRenderer(float, backdropFrame);
14196
+ }).catch(function() {});
14197
+ });
14198
+ container.addEventListener('click', function(event) {
14199
+ if (!event.isTrusted || !float || !float.isConnected) return;
14200
+ event.preventDefault();
14201
+ event.stopImmediatePropagation();
14202
+ }, true);
14203
+ }
14204
+
14205
+ requestAnimationFrame(function() {
14206
+ wireLiquidRailInteractions(document.getElementById('mode-toggle'), '.mode-toggle-btn[data-mode]');
14207
+ wireLiquidRailInteractions(document.getElementById('left-tool-surface'), '.left-nav-icon');
14208
+ wireLiquidRailInteractions(document.getElementById('left-thumb'), '.lt-item');
14209
+ wireLiquidRailInteractions(document.getElementById('left-ws-list'), '.left-ws-item');
14210
+ wireLiquidRailInteractions(document.getElementById('right-tabs'), '.tab-btn[data-tab]');
14211
+ });
14212
+
14213
+ // Checkbox switches use one pointer-captured glass thumb that follows the
14214
+ // pointer continuously, then commits to the nearest binary stop on release.
14215
+ document.addEventListener('pointerdown', function(event) {
14216
+ var input = event.target && event.target.closest ? event.target.closest('input[type="checkbox"]') : null;
14217
+ if (!input || input.disabled || event.button !== 0) return;
14218
+ event.preventDefault();
14219
+ var rect = input.getBoundingClientRect();
14220
+ var pointer = event.pointerId;
14221
+ var fraction = input.checked ? 1 : 0;
14222
+ var startX = event.clientX;
14223
+ var startY = event.clientY;
14224
+ var dragging = false;
14225
+ var canceledToScroll = false;
14226
+ var dragThreshold = event.pointerType === 'mouse' ? 4 : 8;
14227
+ input.dataset.liquidSuppressClick = 'true';
14228
+ clearTimeout(input._liquidSuppressClickTimer);
14229
+ input._liquidSuppressClickTimer = setTimeout(function() {
14230
+ delete input.dataset.liquidSuppressClick;
14231
+ delete input._liquidSuppressClickTimer;
14232
+ }, 700);
14233
+ function update(clientX) {
14234
+ fraction = ((clientX - rect.left - 12) / Math.max(1, rect.width - 24));
14235
+ fraction = Math.max(0, Math.min(1, fraction));
14236
+ input.style.setProperty('--liquid-switch-x', (18 * fraction) + 'px');
14237
+ input.style.setProperty('--liquid-switch-progress', (100 * fraction) + '%');
14238
+ }
14239
+ function finish(commit) {
14240
+ input.removeEventListener('pointermove', move);
14241
+ input.removeEventListener('pointerup', up);
14242
+ input.removeEventListener('pointercancel', cancel);
14243
+ input.classList.remove('liquid-switch-dragging');
14244
+ input.style.removeProperty('--liquid-switch-x');
14245
+ input.style.removeProperty('--liquid-switch-progress');
14246
+ try { input.releasePointerCapture(pointer); } catch (_) {}
14247
+ if (!commit || canceledToScroll) return;
14248
+ var next = dragging ? fraction >= 0.5 : !input.checked;
14249
+ var changed = input.checked !== next;
14250
+ input.checked = next;
14251
+ if (changed) {
14252
+ input.dispatchEvent(new Event('input', { bubbles: true }));
14253
+ input.dispatchEvent(new Event('change', { bubbles: true }));
14254
+ }
14255
+ }
14256
+ function move(moveEvent) {
14257
+ if (moveEvent.pointerId !== pointer) return;
14258
+ if (!dragging) {
14259
+ var deltaX = moveEvent.clientX - startX;
14260
+ var deltaY = moveEvent.clientY - startY;
14261
+ if (Math.hypot(deltaX, deltaY) < dragThreshold) return;
14262
+ if (Math.abs(deltaY) > Math.abs(deltaX)) {
14263
+ canceledToScroll = true;
14264
+ return;
14265
+ }
14266
+ dragging = true;
14267
+ }
14268
+ moveEvent.preventDefault();
14269
+ update(moveEvent.clientX);
14270
+ }
14271
+ function up(upEvent) {
14272
+ if (upEvent.pointerId === pointer) finish(true);
14273
+ }
14274
+ function cancel(cancelEvent) {
14275
+ if (cancelEvent.pointerId === pointer) finish(false);
14276
+ }
14277
+ input.classList.add('liquid-switch-dragging');
14278
+ input.setPointerCapture(pointer);
14279
+ input.addEventListener('pointermove', move);
14280
+ input.addEventListener('pointerup', up);
14281
+ input.addEventListener('pointercancel', cancel);
14282
+ }, true);
14283
+ document.addEventListener('click', function(event) {
14284
+ var input = event.target && event.target.closest ? event.target.closest('input[type="checkbox"]') : null;
14285
+ if (!input || input.dataset.liquidSuppressClick !== 'true') return;
14286
+ clearTimeout(input._liquidSuppressClickTimer);
14287
+ delete input.dataset.liquidSuppressClick;
14288
+ delete input._liquidSuppressClickTimer;
14289
+ event.preventDefault();
14290
+ event.stopImmediatePropagation();
14291
+ }, true);
14292
+
14293
+ // Glass-strength range mirrors the binary liquid switch: its existing color
14294
+ // thumb lifts into one glass capsule, follows the pointer continuously, then
14295
+ // falls back to the color thumb and persists the released value.
14296
+ document.addEventListener('pointerdown', function(event) {
14297
+ var input = event.target && event.target.closest ? event.target.closest('#settings-glass-opacity') : null;
14298
+ if (!input || input.disabled || event.button !== 0) return;
14299
+ event.preventDefault();
14300
+ var rect = input.getBoundingClientRect();
14301
+ var pointer = event.pointerId;
14302
+ var minimum = Number(input.min || 0);
14303
+ var maximum = Number(input.max || 100);
14304
+ var step = Math.max(0.0001, Number(input.step || 1));
14305
+ var initialValue = input.value;
14306
+ function update(clientX) {
14307
+ var fraction = Math.max(0, Math.min(1, (clientX - rect.left) / Math.max(1, rect.width)));
14308
+ var raw = minimum + fraction * (maximum - minimum);
14309
+ var next = Math.min(maximum, Math.max(minimum, Math.round((raw - minimum) / step) * step + minimum));
14310
+ input.value = String(next);
14311
+ input.style.setProperty('--liquid-range-progress', (fraction * 100) + '%');
14312
+ input.dispatchEvent(new Event('input', { bubbles: true }));
14313
+ }
14314
+ function finish(commit) {
14315
+ input.removeEventListener('pointermove', move);
14316
+ input.removeEventListener('pointerup', up);
14317
+ input.removeEventListener('pointercancel', cancel);
14318
+ input.classList.remove('liquid-range-dragging');
14319
+ try { input.releasePointerCapture(pointer); } catch (_) {}
14320
+ if (!commit) {
14321
+ input.value = initialValue;
14322
+ input.dispatchEvent(new Event('input', { bubbles: true }));
14323
+ return;
14324
+ }
14325
+ input.dataset.liquidSuppressRangeClick = 'true';
14326
+ input.dispatchEvent(new Event('change', { bubbles: true }));
14327
+ }
14328
+ function move(moveEvent) { if (moveEvent.pointerId === pointer) { moveEvent.preventDefault(); update(moveEvent.clientX); } }
14329
+ function up(upEvent) { if (upEvent.pointerId === pointer) finish(true); }
14330
+ function cancel(cancelEvent) { if (cancelEvent.pointerId === pointer) finish(false); }
14331
+ input.classList.add('liquid-range-dragging');
14332
+ update(event.clientX);
14333
+ input.setPointerCapture(pointer);
14334
+ input.addEventListener('pointermove', move);
14335
+ input.addEventListener('pointerup', up);
14336
+ input.addEventListener('pointercancel', cancel);
14337
+ }, true);
14338
+ document.addEventListener('click', function(event) {
14339
+ var input = event.target && event.target.closest ? event.target.closest('#settings-glass-opacity') : null;
14340
+ if (!input || input.dataset.liquidSuppressRangeClick !== 'true') return;
14341
+ delete input.dataset.liquidSuppressRangeClick;
14342
+ event.preventDefault();
14343
+ event.stopImmediatePropagation();
14344
+ }, true);
14345
+
12698
14346
  window.syncModelSelectSurface = function() {
12699
14347
  var sel = els['model-select'] || document.getElementById('model-select');
12700
14348
  var label = document.getElementById('model-select-label');
@@ -12716,6 +14364,7 @@ window.syncModelSelectSurface = function() {
12716
14364
  }
12717
14365
  });
12718
14366
  menu.innerHTML = html;
14367
+ wireLiquidMenuInteractions(menu);
12719
14368
  };
12720
14369
 
12721
14370
  document.addEventListener('pointerdown', function(event) {
@@ -12727,6 +14376,7 @@ window.closeNewmarkSelect = function(shell, options) {
12727
14376
  shell = shell || document.querySelector('.newmark-select-shell.open');
12728
14377
  if (!shell) return;
12729
14378
  shell.classList.remove('open');
14379
+ window.setLiquidSidebarGestureLock('popup:select:' + (shell.dataset.selectId || 'anonymous'), false);
12730
14380
  var menu = shell.querySelector('.newmark-select-menu');
12731
14381
  if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
12732
14382
  var button = shell.querySelector('.newmark-select-button');
@@ -12810,6 +14460,7 @@ window.syncNewmarkSelect = function(select) {
12810
14460
  }
12811
14461
  });
12812
14462
  menu.innerHTML = html;
14463
+ wireLiquidMenuInteractions(menu);
12813
14464
  };
12814
14465
 
12815
14466
  window.enhanceNewmarkSelect = function(select) {
@@ -12850,7 +14501,7 @@ window.enhanceNewmarkSelect = function(select) {
12850
14501
  button.setAttribute('aria-label', String(accessibleLabel).trim());
12851
14502
  button.innerHTML = '<span class="newmark-select-button-label"></span>' + iconSvg('chevron-down', '', 'tiny');
12852
14503
  var menu = document.createElement('div');
12853
- menu.className = 'model-select-menu newmark-select-menu';
14504
+ menu.className = 'model-select-menu newmark-select-menu liquid-glass liquid-glass-carrier';
12854
14505
  menu.setAttribute('role', 'listbox');
12855
14506
  menu.setAttribute('popover', 'manual');
12856
14507
  select.parentNode.insertBefore(shell, select);
@@ -12871,6 +14522,7 @@ window.enhanceNewmarkSelect = function(select) {
12871
14522
  }
12872
14523
  window.syncNewmarkSelect(select);
12873
14524
  shell.classList.add('open');
14525
+ window.setLiquidSidebarGestureLock('popup:select:' + (shell.dataset.selectId || 'anonymous'), true);
12874
14526
  button.setAttribute('aria-expanded', 'true');
12875
14527
  if (opening) {
12876
14528
  if (typeof menu.showPopover === 'function' && !menu.matches(':popover-open')) menu.showPopover();
@@ -13169,6 +14821,47 @@ function backendQueueForTarget(target) {
13169
14821
  return normalizeBackendQueue(state.backendQueuesByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)]);
13170
14822
  }
13171
14823
 
14824
+ function queueItemsForTarget(target) {
14825
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
14826
+ if (!owner) return [];
14827
+ if (!state.queueItemsByTarget) state.queueItemsByTarget = {};
14828
+ return state.queueItemsByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)] || [];
14829
+ }
14830
+
14831
+ function setQueueItemsForTarget(items, target) {
14832
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
14833
+ if (!owner) return;
14834
+ if (!state.queueItemsByTarget) state.queueItemsByTarget = {};
14835
+ state.queueItemsByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)] = (Array.isArray(items) ? items : []).map(function(item) {
14836
+ return {
14837
+ id: String(item && item.id || ''),
14838
+ text: String(item && item.text || ''),
14839
+ queueMode: String(item && item.queueMode || 'followUp'),
14840
+ requestedMode: String(item && item.requestedMode || 'build'),
14841
+ goalObjective: String(item && item.goalObjective || ''),
14842
+ runId: String(item && item.runId || ''),
14843
+ createdAt: String(item && item.createdAt || ''),
14844
+ };
14845
+ });
14846
+ }
14847
+
14848
+ function queueItemIdForText(text, target) {
14849
+ var items = queueItemsForTarget(target);
14850
+ var normalized = String(text || '');
14851
+ for (var i = 0; i < items.length; i++) {
14852
+ if (String(items[i].text || '') === normalized) return items[i].id;
14853
+ }
14854
+ return '';
14855
+ }
14856
+
14857
+ function syncQueueItemsFromSnapshot(snapshot, target) {
14858
+ if (snapshot && Array.isArray(snapshot.queueItems)) setQueueItemsForTarget(snapshot.queueItems, target);
14859
+ }
14860
+
14861
+ window.queueAction = function(action, input) {
14862
+ if (!api.queueAction) return Promise.resolve({ ok: false, error: 'queueAction unavailable' });
14863
+ return api.queueAction(action, input);
14864
+ };
13172
14865
  function setBackendQueueForTarget(queue, target) {
13173
14866
  var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
13174
14867
  var normalized = normalizeBackendQueue(queue);
@@ -13248,19 +14941,22 @@ window.renderQueuePanel = function() {
13248
14941
  if (queueIndex === state._editingQueueIndex) continue;
13249
14942
  var request = state.nextQueueRequests[queueIndex];
13250
14943
  var backendManaged = queuedRequestIsBackendManaged(request);
13251
- var dragAttributes = backendManaged
13252
- ? ' draggable="false"'
13253
- : ' draggable="true" ondragstart="window.startQueueDrag(event,' + queueIndex + ')" ondragover="window.overQueueDrag(event,' + queueIndex + ')" ondrop="window.dropQueueDrag(event,' + queueIndex + ')" ondragend="window.endQueueDrag(event)"';
13254
- var dragHandle = backendManaged
13255
- ? '<span class="queue-drag" aria-hidden="true">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</span>'
13256
- : '<button class="queue-drag" title="' + escAttr(t('queue.title')) + '">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</button>';
13257
- var inputAttributes = backendManaged
13258
- ? ' readonly aria-readonly="true"'
13259
- : ' readonly aria-readonly="true"';
13260
- var actions = backendManaged ? '' :
14944
+ // dev-0.5.5: a backend-managed item is editable/deletable when the kernel
14945
+ // exposed its stable queueItem id (mobile-originated queue entries now
14946
+ // carry ids through queueItems). Items without an id stay locked.
14947
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[queueIndex] || '', currentConversationTarget()) : '';
14948
+ var operable = !backendManaged || !!queueItemId;
14949
+ var dragAttributes = operable
14950
+ ? ' draggable="true" ondragstart="window.startQueueDrag(event,' + queueIndex + ')" ondragover="window.overQueueDrag(event,' + queueIndex + ')" ondrop="window.dropQueueDrag(event,' + queueIndex + ')" ondragend="window.endQueueDrag(event)"'
14951
+ : ' draggable="false"';
14952
+ var dragHandle = operable
14953
+ ? '<button class="queue-drag" title="' + escAttr(t('queue.title')) + '">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</button>'
14954
+ : '<span class="queue-drag" aria-hidden="true">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</span>';
14955
+ var inputAttributes = ' readonly aria-readonly="true"';
14956
+ var actions = operable ?
13261
14957
  '<button type="button" class="stack-icon-btn queue-guide-btn" onclick="window.guideQueueItem(' + queueIndex + ')" title="' + escAttr(t('queue.guideNow')) + '">' + iconSvg('corner-down-right', t('queue.guideNow'), 'tiny') + '<span>' + esc(t('queue.guideAction')) + '</span></button>' +
13262
14958
  '<button class="stack-icon-btn" onclick="window.focusQueueItem(' + queueIndex + ')" title="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</button>' +
13263
- '<button class="stack-icon-btn danger" onclick="window.deleteQueueItem(' + queueIndex + ')" title="' + escAttr(t('common.delete')) + '">' + iconSvg('trash', t('common.delete'), 'tiny') + '</button>';
14959
+ '<button class="stack-icon-btn danger" onclick="window.deleteQueueItem(' + queueIndex + ')" title="' + escAttr(t('common.delete')) + '">' + iconSvg('trash', t('common.delete'), 'tiny') + '</button>' : '';
13264
14960
  html += '<div class="queue-item"' + dragAttributes + '>' +
13265
14961
  dragHandle +
13266
14962
  '<input class="queue-edit" value="' + escAttr(state.nextQueue[queueIndex] || '') + '"' + inputAttributes + '>' +
@@ -13293,9 +14989,11 @@ window.editQueueItem = function(idx, value) {
13293
14989
 
13294
14990
  window.focusQueueItem = function(idx) {
13295
14991
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13296
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) return;
14992
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
14993
+ var backendManaged = queuedRequestIsBackendManaged(request);
14994
+ if (backendManaged && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget())) return;
13297
14995
  state._editingQueueIndex = idx;
13298
- state.pendingInputEdit = { kind: 'queue', index: idx, request: request, target: currentConversationTarget() };
14996
+ state.pendingInputEdit = { kind: 'queue', index: idx, request: request, target: currentConversationTarget(), originalText: String(state.nextQueue[idx] || '') };
13299
14997
  els.prompt.value = String(state.nextQueue[idx] || '');
13300
14998
  state.promptAttachments = (request.images || []).map(function(image, imageIndex) {
13301
14999
  return { id: 'queue-edit-' + imageIndex + '-' + Date.now(), name: image.name || 'queued image', type: image.type || 'image/png', dataUrl: image.dataUrl };
@@ -13309,13 +15007,23 @@ window.focusQueueItem = function(idx) {
13309
15007
  window.deleteQueueItem = function(idx) {
13310
15008
  if (!state.nextQueue) return;
13311
15009
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13312
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15010
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15011
+ var backendManaged = queuedRequestIsBackendManaged(request);
15012
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()) : '';
15013
+ if (backendManaged && !queueItemId) return;
13313
15014
  if (!state.queueHiddenItems) state.queueHiddenItems = {};
13314
15015
  if (state.nextQueue[idx]) state.queueHiddenItems[queueHiddenItemKey(state.nextQueue[idx], currentConversationTarget())] = true;
13315
15016
  state.nextQueue.splice(idx, 1);
13316
15017
  if (state.nextQueueRequests) state.nextQueueRequests.splice(idx, 1);
13317
15018
  refreshNextPromptForTarget(currentConversationTarget());
13318
15019
  window.renderInputStack();
15020
+ if (backendManaged) {
15021
+ window.queueAction('queue_delete', { id: queueItemId }).then(function(result) {
15022
+ if (!result || result.ok === false) {
15023
+ showUiNotice(result && result.error ? result.error : (currentLang() === 'zh' ? '删除队列项失败' : 'Failed to delete queue item'), 'error', 'queue-delete-' + queueItemId);
15024
+ }
15025
+ });
15026
+ }
13319
15027
  };
13320
15028
 
13321
15029
  function restoreQueueItemAfterGuideFailure(idx, text, request, target, reuseClientMessageId) {
@@ -13332,7 +15040,10 @@ function restoreQueueItemAfterGuideFailure(idx, text, request, target, reuseClie
13332
15040
  window.guideQueueItem = function(idx) {
13333
15041
  if (!state.nextQueue || !state.nextQueue[idx]) return;
13334
15042
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13335
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15043
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15044
+ var backendManaged = queuedRequestIsBackendManaged(request);
15045
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()) : '';
15046
+ if (backendManaged && !queueItemId) return;
13336
15047
  var target = normalizeQueuedConversationTarget(request.target) || currentConversationTarget();
13337
15048
  var deliverDuringRun = isCurrentConversationRunning();
13338
15049
  var next = state.nextQueue.splice(idx, 1)[0];
@@ -13341,6 +15052,20 @@ window.guideQueueItem = function(idx) {
13341
15052
  if (next) state.queueHiddenItems[queueHiddenItemKey(next, target)] = true;
13342
15053
  refreshNextPromptForTarget(currentConversationTarget());
13343
15054
  window.renderInputStack();
15055
+ if (backendManaged) {
15056
+ // The kernel owns this entry; guide it there so the mobile side stays
15057
+ // in sync. On failure restore the local row.
15058
+ return window.queueAction('queue_guide', { id: queueItemId }).then(function(outcome) {
15059
+ if (!outcome || outcome.ok === false) {
15060
+ restoreQueueItemAfterGuideFailure(idx, next, nextRequest, target, true);
15061
+ showUiNotice(outcome && outcome.error ? outcome.error : (currentLang() === 'zh' ? 'Guide 投递失败' : 'Guide delivery failed'), 'error', 'queue-guide-' + queueItemId);
15062
+ }
15063
+ return outcome || { ok: false };
15064
+ }, function(error) {
15065
+ restoreQueueItemAfterGuideFailure(idx, next, nextRequest, target, true);
15066
+ return { ok: false, error: error && error.message ? error.message : String(error) };
15067
+ });
15068
+ }
13344
15069
  if (next) {
13345
15070
  var delivery = window.sendMessage('guide', next, {
13346
15071
  fromQueue: true,
@@ -13365,7 +15090,11 @@ window.guideQueueItem = function(idx) {
13365
15090
 
13366
15091
  window.startQueueDrag = function(event, idx) {
13367
15092
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13368
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) {
15093
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) {
15094
+ state.queueDragIndex = -1;
15095
+ return;
15096
+ }
15097
+ if (queuedRequestIsBackendManaged(request) && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget())) {
13369
15098
  state.queueDragIndex = -1;
13370
15099
  return;
13371
15100
  }
@@ -13386,9 +15115,12 @@ window.dropQueueDrag = function(event, idx) {
13386
15115
  if (!state.nextQueue || from < 0 || from === idx || from >= state.nextQueue.length) return;
13387
15116
  var sourceRequest = state.nextQueueRequests && state.nextQueueRequests[from];
13388
15117
  var destinationRequest = state.nextQueueRequests && state.nextQueueRequests[idx];
13389
- if (queuedRequestIsBackendManaged(sourceRequest) || queuedRequestIsBackendManaged(destinationRequest)
13390
- || !queuedRequestMatchesTarget(sourceRequest, currentConversationTarget())
15118
+ if (!queuedRequestMatchesTarget(sourceRequest, currentConversationTarget())
13391
15119
  || !queuedRequestMatchesTarget(destinationRequest, currentConversationTarget())) return;
15120
+ var sourceBackend = queuedRequestIsBackendManaged(sourceRequest);
15121
+ var destinationBackend = queuedRequestIsBackendManaged(destinationRequest);
15122
+ if ((sourceBackend && !queueItemIdForText(state.nextQueue[from] || '', currentConversationTarget()))
15123
+ || (destinationBackend && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()))) return;
13392
15124
  var item = state.nextQueue.splice(from, 1)[0];
13393
15125
  var requestItem = state.nextQueueRequests ? state.nextQueueRequests.splice(from, 1)[0] : null;
13394
15126
  state.nextQueue.splice(idx, 0, item);
@@ -13396,6 +15128,17 @@ window.dropQueueDrag = function(event, idx) {
13396
15128
  state.queueDragIndex = -1;
13397
15129
  refreshNextPromptForTarget(currentConversationTarget());
13398
15130
  window.renderInputStack();
15131
+ if (sourceBackend || destinationBackend) {
15132
+ // The kernel owns at least one affected entry: forward the complete
15133
+ // ordered id list so mobile and desktop stay in sync.
15134
+ var visibleIndexes = queueIndexesForTarget(currentConversationTarget());
15135
+ var orderedIds = visibleIndexes.map(function(vi) {
15136
+ return queueItemIdForText(state.nextQueue[vi] || '', currentConversationTarget());
15137
+ }).filter(function(id) { return !!id; });
15138
+ if (orderedIds.length) {
15139
+ window.queueAction('queue_reorder', { orderedIds: orderedIds });
15140
+ }
15141
+ }
13399
15142
  };
13400
15143
 
13401
15144
  window.endQueueDrag = function(event) {
@@ -13445,7 +15188,24 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13445
15188
  els.prompt.dispatchEvent(new Event('input', { bubbles: true }));
13446
15189
  if (pendingEdit.kind === 'queue') {
13447
15190
  var editIndex = Number(pendingEdit.index);
15191
+ var editRequest = pendingEdit.request || (state.nextQueueRequests && state.nextQueueRequests[editIndex]);
15192
+ var editBackendManaged = queuedRequestIsBackendManaged(editRequest);
15193
+ var editQueueItemId = editBackendManaged ? queueItemIdForText(String(state.nextQueue[editIndex] || pendingEdit.originalText || ''), pendingEdit.target) : '';
15194
+ if (editBackendManaged && !editQueueItemId) {
15195
+ showUiNotice(currentLang() === 'zh' ? '无法编辑该队列项:缺少后端 id' : 'Cannot edit queue item: backend id missing', 'error', 'queue-edit-' + editIndex);
15196
+ return { ok: false, error: 'missing backend queue id' };
15197
+ }
13448
15198
  if (effectiveInputMode === 'next') {
15199
+ if (editBackendManaged) {
15200
+ // The kernel owns this entry: forward the edit and let the next
15201
+ // snapshot reconcile the local row.
15202
+ window.queueAction('queue_update', { id: editQueueItemId, text: editedValue, requestedMode: String(editRequest && editRequest.requestedMode || 'build'), goalObjective: String(editRequest && editRequest.goalObjective || ''), createdAt: String(editRequest && editRequest.createdAt || '') });
15203
+ state.nextQueue[editIndex] = editedValue;
15204
+ if (state.nextQueueRequests[editIndex]) state.nextQueueRequests[editIndex].text = editedValue;
15205
+ refreshNextPromptForTarget(pendingEdit.target);
15206
+ window.renderInputStack();
15207
+ return { ok: true, queued: true };
15208
+ }
13449
15209
  state.nextQueue[editIndex] = editedValue;
13450
15210
  if (state.nextQueueRequests[editIndex]) {
13451
15211
  state.nextQueueRequests[editIndex].text = editedValue;
@@ -13808,7 +15568,11 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13808
15568
  }));
13809
15569
  var lockedUi = conversationWorkUiState(lockedConversationId, lockedTarget.workspaceId);
13810
15570
  if (stillActive && r && Array.isArray(r.diffs) && r.diffs.length) {
13811
- lockedUi.pendingWorkReview = r.diffs;
15571
+ var reviewRecord = {
15572
+ diffs: r.diffs,
15573
+ runId: String(r.runId || localRunId || '')
15574
+ };
15575
+ upsertWorkReviewRecord(lockedUi, reviewRecord.diffs, reviewRecord.runId, [localRunId]);
13812
15576
  }
13813
15577
  if (stillActive && !responseOwnedByRun) {
13814
15578
  responseMsg = lockedUi.activeWorkflowMsg;
@@ -13837,9 +15601,13 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13837
15601
  hydrateConversationBranchState(r);
13838
15602
  renderChatMessages(r.chatMessages);
13839
15603
  }
13840
- else if (stillActive) {
15604
+ // Re-render every retained completed-run review after a transcript rebuild.
15605
+ // addWorkReview dedupes by run identity and normalized file totals.
15606
+ if (stillActive) {
13841
15607
  var completedReviewUi = conversationWorkUiState(lockedConversationId);
13842
- if (completedReviewUi.pendingWorkReview) addWorkReview(completedReviewUi.pendingWorkReview);
15608
+ (completedReviewUi.workReviews || []).forEach(function(item) {
15609
+ addWorkReview(item.diffs || item, item.runId || '', [localRunId]);
15610
+ });
13843
15611
  }
13844
15612
  if (r && r.conversations) {
13845
15613
  applyBackendConversations(r.conversations || [], stillActive ? lockedConversationId : activeConversationId(), lockedTarget.workspaceId);
@@ -13860,6 +15628,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13860
15628
  if (r && r.queued) {
13861
15629
  var responseQueue = setBackendQueueForTarget(r.queued, lockedTarget);
13862
15630
  window.syncNextQueueFromBackend(responseQueue, lockedTarget);
15631
+ syncQueueItemsFromSnapshot(r, lockedTarget);
13863
15632
  if (stillActive) state.backendQueue = responseQueue;
13864
15633
  }
13865
15634
  if (r && r.options) {
@@ -13905,6 +15674,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13905
15674
  if (s && s.queued) {
13906
15675
  var refreshedQueue = setBackendQueueForTarget(s.queued, lockedTarget);
13907
15676
  window.syncNextQueueFromBackend(refreshedQueue, lockedTarget);
15677
+ syncQueueItemsFromSnapshot(s, lockedTarget);
13908
15678
  if (stillActiveAfterRefresh) state.backendQueue = refreshedQueue;
13909
15679
  }
13910
15680
  if (s && s.conversations) {
@@ -14349,6 +16119,7 @@ window.switchRightTab = function(tab) {
14349
16119
  var rightTabs = els['right-tabs'] || document.getElementById('right-tabs');
14350
16120
  var rightContent = els['right-content'] || document.getElementById('right-content');
14351
16121
  if (!rightTabs || !rightContent) return;
16122
+ wireLiquidRailInteractions(rightTabs, '.tab-btn[data-tab]');
14352
16123
  var btns = rightTabs.querySelectorAll('.tab-btn[data-tab]');
14353
16124
  for (var i = 0; i < btns.length; i++) {
14354
16125
  var selected = btns[i].getAttribute('data-tab') === tab;
@@ -15257,7 +17028,7 @@ if (api.onTerminalTakeover) {
15257
17028
  if (api.terminalTakeoverState) window.refreshTerminalTakeoverPanes();
15258
17029
 
15259
17030
  // === Scroll to Bottom ===
15260
- var _chatScrollNearBottomPx = 80;
17031
+ var _chatScrollNearBottomPx = 24;
15261
17032
  var _chatShouldAutoScroll = true;
15262
17033
  function shouldAutoScroll(el) { return el.scrollHeight - el.scrollTop - el.clientHeight <= _chatScrollNearBottomPx; }
15263
17034
  function autoScrollIfAtBottom() { var el = els['chat-area']; if (!el) return; if (_chatShouldAutoScroll) el.scrollTop = el.scrollHeight; }
@@ -15547,6 +17318,7 @@ window.openSubWin = function(title, html) {
15547
17318
  els['sub-win-title'].textContent = title;
15548
17319
  els['sub-win-body'].innerHTML = html;
15549
17320
  els['sub-win'].classList.toggle('memory-lab-window', !!(state.activeSubWindowView && state.activeSubWindowView.name === 'memoryLab'));
17321
+ els['sub-win'].classList.toggle('settings-window', !!(state.activeSubWindowView && state.activeSubWindowView.name === 'settings'));
15550
17322
  els['sub-win-overlay'].classList.add('open');
15551
17323
  updateApplicationInertState();
15552
17324
  requestAnimationFrame(focusSubWindowInitial);
@@ -15577,6 +17349,7 @@ window.closeSubWin = function() {
15577
17349
  els['sub-win-title'].textContent = previous.title;
15578
17350
  els['sub-win-body'].innerHTML = previous.html;
15579
17351
  els['sub-win'].classList.toggle('memory-lab-window', !!(previous.view && previous.view.name === 'memoryLab'));
17352
+ els['sub-win'].classList.toggle('settings-window', !!(previous.view && previous.view.name === 'settings'));
15580
17353
  requestAnimationFrame(function() {
15581
17354
  els['sub-win-body'].scrollTop = previous.scrollTop || 0;
15582
17355
  state.restoringSubWindow = false;
@@ -15587,7 +17360,7 @@ window.closeSubWin = function() {
15587
17360
  els['sub-win-overlay'].classList.remove('open');
15588
17361
  state.activeSubWindowView = null;
15589
17362
  var win = els['sub-win'];
15590
- if (win) { win.classList.remove('memory-lab-window'); win.style.left = ''; win.style.top = ''; win.style.margin = ''; }
17363
+ if (win) { win.classList.remove('memory-lab-window', 'settings-window'); win.style.left = ''; win.style.top = ''; win.style.margin = ''; }
15591
17364
  var restoreFocus = subWindowOriginFocus;
15592
17365
  subWindowOriginFocus = null;
15593
17366
  updateApplicationInertState();
@@ -15660,7 +17433,10 @@ window.openSettings = function(tab) {
15660
17433
  if (refreshingSettings) state.restoringSubWindow = true;
15661
17434
  window.openSubWin(t('settings.title'), html);
15662
17435
  state.restoringSubWindow = false;
15663
- requestAnimationFrame(function() { window.settingsTab(activeTab); });
17436
+ requestAnimationFrame(function() {
17437
+ window.settingsTab(activeTab);
17438
+ wireLiquidRailInteractions(document.querySelector('.settings-tabs:not(.plugin-tabs)'), '.stab-btn[data-stab]');
17439
+ });
15664
17440
  };
15665
17441
 
15666
17442
  function renderGeneralSettings() {
@@ -15740,7 +17516,7 @@ function renderGeneralSettings() {
15740
17516
  '</div>' +
15741
17517
  '<div class="setting-row">' +
15742
17518
  '<label class="setting-label" for="settings-glass-opacity">' + esc(t('settings.glass')) + '</label>' +
15743
- '<div class="setting-control"><input id="settings-glass-opacity" type="range" min="0" max="100" step="1" value="' + glassVal + '" aria-describedby="glass-desc" aria-valuetext="' + escAttr(glassDescription) + '" oninput="window.previewGlassOpacity(this.value)" onchange="window.commitGlassOpacity(this.value)">' +
17519
+ '<div class="setting-control"><div class="settings-glass-range-line"><input id="settings-glass-opacity" type="range" min="0" max="100" step="1" value="' + glassVal + '" style="--liquid-range-progress:' + glassVal + '%" aria-describedby="glass-desc settings-glass-value" aria-valuetext="' + escAttr(glassDescription) + '" oninput="window.previewGlassOpacity(this.value)" onchange="window.commitGlassOpacity(this.value)"><output id="settings-glass-value" for="settings-glass-opacity">' + glassNumberText(glassVal) + '%</output></div>' +
15744
17520
  '<div class="setting-desc" id="glass-desc" aria-live="polite">' + esc(glassDescription) + '</div></div>' +
15745
17521
  '</div>' +
15746
17522
  '<div class="setting-row">' +
@@ -16240,6 +18016,7 @@ function glassPresentationForOpacity(value) {
16240
18016
  var alpha = opacityPercent / 100;
16241
18017
  var transparencyPercent = 100 - opacityPercent;
16242
18018
  var glassWidth = 20 * transparencyPercent / 100;
18019
+ var opticalStrength = Math.min(2, transparencyPercent / 15);
16243
18020
  var rounded = function(number) { return Math.round(number * 10000) / 10000; };
16244
18021
  return {
16245
18022
  opacityPercent: rounded(opacityPercent),
@@ -16248,6 +18025,12 @@ function glassPresentationForOpacity(value) {
16248
18025
  blur1: rounded(0.4 * glassWidth),
16249
18026
  blur2: rounded(0.8 * glassWidth),
16250
18027
  blur3: rounded(glassWidth),
18028
+ carrierBlur: rounded(glassWidth * 2.2),
18029
+ carrierAlpha: rounded(Math.max(0.08, 0.44 * alpha)),
18030
+ floatRefractionHeight: rounded(12 * opticalStrength),
18031
+ floatRefractionAmount: rounded(24 * opticalStrength),
18032
+ floatChromaticAberration: rounded(opticalStrength),
18033
+ floatOpticalOpacity: rounded(Math.min(1, opticalStrength)),
16251
18034
  alpha1: rounded(0.75 * alpha),
16252
18035
  alpha2: rounded(0.80 * alpha),
16253
18036
  alpha3: rounded(0.85 * alpha),
@@ -16280,11 +18063,21 @@ function applyGlassPresentation(presentation) {
16280
18063
  root.style.setProperty('--glass-blur-2', presentation.blur2 + 'px');
16281
18064
  root.style.setProperty('--glass-blur-3', presentation.blur3 + 'px');
16282
18065
  }
18066
+ root.style.setProperty('--carrier-glass-blur', presentation.carrierBlur + 'px');
18067
+ root.style.setProperty('--carrier-glass-alpha', String(presentation.carrierAlpha));
18068
+ root.style.setProperty('--liquid-float-refraction-amount', presentation.floatRefractionAmount + 'px');
18069
+ root.style.setProperty('--liquid-float-chromatic-aberration', String(presentation.floatChromaticAberration));
18070
+ root.style.setProperty('--liquid-float-optical-opacity', String(presentation.floatOpticalOpacity));
16283
18071
  var description = glassDescriptionText(presentation);
16284
18072
  var desc = document.getElementById('glass-desc');
16285
18073
  if (desc) desc.textContent = description;
16286
18074
  var slider = document.getElementById('settings-glass-opacity');
16287
- if (slider) slider.setAttribute('aria-valuetext', description);
18075
+ if (slider) {
18076
+ slider.setAttribute('aria-valuetext', description);
18077
+ slider.style.setProperty('--liquid-range-progress', presentation.opacityPercent + '%');
18078
+ }
18079
+ var valueOutput = document.getElementById('settings-glass-value');
18080
+ if (valueOutput) valueOutput.textContent = glassNumberText(presentation.opacityPercent) + '%';
16288
18081
  return presentation;
16289
18082
  }
16290
18083
 
@@ -19431,14 +21224,25 @@ function pendingConversationActivation(target) {
19431
21224
  }
19432
21225
 
19433
21226
  function applyBackendConversations(items, activeId, workspaceId) {
19434
- if (!state.currentWorkspace || !items || !items.length) return;
21227
+ if (!state.currentWorkspace) return;
19435
21228
  var key = currentWorkspaceKey(workspaceId);
19436
21229
  var isForegroundWorkspace = key === currentWorkspaceKey();
19437
21230
  var existingConversations = state.workspaceConversations[key] || [];
19438
- var convs = [];
19439
- var requestedActiveId = String(activeId || '');
19440
21231
  var holdId = String(state.foregroundConversationHoldId || '');
19441
21232
  var useHold = !!(isForegroundWorkspace && holdId && Date.now() < Number(state.foregroundConversationHoldUntil || 0));
21233
+ if ((!items || !items.length) && !useHold) {
21234
+ // 后端权威列表为空:工作区对话确实已清空。保持为空,并移除本地
21235
+ // 自动兜底的 default 对话,跨重启/切换工作区后也不会复活。
21236
+ state.workspaceConversations[key] = [];
21237
+ state.workspaceActiveConversation[key] = -1;
21238
+ if (isForegroundWorkspace) {
21239
+ state.conversations = [];
21240
+ state.activeConversation = -1;
21241
+ }
21242
+ return;
21243
+ }
21244
+ var convs = [];
21245
+ var requestedActiveId = String(activeId || '');
19442
21246
  var seenIds = {};
19443
21247
  for (var i = 0; i < items.length; i++) {
19444
21248
  var item = items[i] || {};
@@ -19564,16 +21368,244 @@ function appendDomNodesInBatches(container, nodes, batchKey, batchSize) {
19564
21368
  appendBatch();
19565
21369
  }
19566
21370
 
21371
+ var conversationGlassFlight = { float: null, source: null, covered: null, timer: 0, generation: 0 };
21372
+ var conversationNativeDrag = {
21373
+ row: null,
21374
+ float: null,
21375
+ arrived: false,
21376
+ grabX: 0,
21377
+ grabY: 0,
21378
+ clientX: 0,
21379
+ clientY: 0
21380
+ };
21381
+
21382
+ function positionConversationGlassAroundRow(float, row) {
21383
+ if (!float || !row) return;
21384
+ var rect = row.getBoundingClientRect();
21385
+ var edge = liquidInteractionEdgePx();
21386
+ keepLiquidFloatExpansionInsideViewport(float, rect);
21387
+ float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
21388
+ float.style.left = (rect.left - edge) + 'px';
21389
+ float.style.top = (rect.top - edge) + 'px';
21390
+ float.style.width = (rect.width + edge * 2) + 'px';
21391
+ float.style.height = (rect.height + edge * 2) + 'px';
21392
+ if (float._renderKyantGlass) float._renderKyantGlass({
21393
+ left: rect.left - edge, top: rect.top - edge,
21394
+ width: rect.width + edge * 2, height: rect.height + edge * 2
21395
+ }, true);
21396
+ }
21397
+
21398
+ function followConversationDragPointer() {
21399
+ var drag = conversationNativeDrag;
21400
+ if (!drag.arrived || !drag.float) return;
21401
+ var list = els['conversation-list'];
21402
+ if (!list || !drag.row) return;
21403
+ var conversations = currentWorkspaceConversations();
21404
+ var draggedId = String(drag.row.getAttribute('data-conversation-id') || '');
21405
+ var draggedConversation = conversations.find(function(item) { return String(item && item.id || '') === draggedId; });
21406
+ var trackRows = Array.from(list.querySelectorAll('.conv-item')).filter(function(row) {
21407
+ var id = String(row.getAttribute('data-conversation-id') || '');
21408
+ var conversation = conversations.find(function(item) { return String(item && item.id || '') === id; });
21409
+ return conversation && draggedConversation && !!conversation.pinned === !!draggedConversation.pinned;
21410
+ });
21411
+ if (!trackRows.length) trackRows = [drag.row];
21412
+ var firstRect = trackRows[0].getBoundingClientRect();
21413
+ var lastRect = trackRows[trackRows.length - 1].getBoundingClientRect();
21414
+ var rowRect = drag.row.getBoundingClientRect();
21415
+ var projectedTop = drag.clientY - drag.grabY;
21416
+ var minTop = firstRect.top - rowRect.height;
21417
+ var maxTop = Math.max(minTop, lastRect.bottom);
21418
+ var trackTop = Math.max(minTop, Math.min(maxTop, projectedTop));
21419
+ var edge = liquidInteractionEdgePx();
21420
+ drag.float.style.left = (rowRect.left - edge) + 'px';
21421
+ drag.float.style.top = (trackTop - edge) + 'px';
21422
+ if (drag.float._renderKyantGlass) drag.float._renderKyantGlass();
21423
+ }
21424
+
21425
+ function wrapConversationDragContent(row, float) {
21426
+ if (!row || !float || !row.isConnected) return;
21427
+ var content = document.createElement('div');
21428
+ content.className = 'conversation-drag-content';
21429
+ Array.prototype.slice.call(row.childNodes).forEach(function(node) { content.appendChild(node.cloneNode(true)); });
21430
+ float.appendChild(content);
21431
+ row.classList.add('dragging', 'conversation-glass-dragging');
21432
+ float.classList.add('conversation-drag-following');
21433
+ conversationNativeDrag.arrived = true;
21434
+ conversationNativeDrag.float = float;
21435
+ followConversationDragPointer();
21436
+ }
21437
+
21438
+ window.updateConversationNativeDrag = function(clientX, clientY) {
21439
+ conversationNativeDrag.clientX = clientX;
21440
+ conversationNativeDrag.clientY = clientY;
21441
+ var drag = conversationNativeDrag;
21442
+ var list = els['conversation-list'];
21443
+ if (drag.row && list) {
21444
+ var conversations = currentWorkspaceConversations();
21445
+ var draggedId = String(drag.row.getAttribute('data-conversation-id') || '');
21446
+ var draggedConversation = conversations.find(function(item) { return String(item && item.id || '') === draggedId; });
21447
+ var peers = Array.from(list.querySelectorAll('.conv-item')).filter(function(row) {
21448
+ if (row === drag.row) return false;
21449
+ var id = String(row.getAttribute('data-conversation-id') || '');
21450
+ var conversation = conversations.find(function(item) { return String(item && item.id || '') === id; });
21451
+ return conversation && draggedConversation && !!conversation.pinned === !!draggedConversation.pinned;
21452
+ });
21453
+ if (peers.length) {
21454
+ var firstPeerRect = peers[0].getBoundingClientRect();
21455
+ var lastPeerRect = peers[peers.length - 1].getBoundingClientRect();
21456
+ if (clientY <= firstPeerRect.top) {
21457
+ list.insertBefore(drag.row, peers[0]);
21458
+ } else if (clientY >= lastPeerRect.bottom) {
21459
+ list.insertBefore(drag.row, peers[peers.length - 1].nextSibling);
21460
+ }
21461
+ }
21462
+ }
21463
+ followConversationDragPointer();
21464
+ };
21465
+
21466
+ // Native conversation drags can leave the list while the mouse button remains
21467
+ // held. Track at document capture scope so the carried glass never freezes at
21468
+ // the list boundary; drop/reorder ownership remains scoped to the list itself.
21469
+ document.addEventListener('dragover', function(event) {
21470
+ if (conversationNativeDrag.row) window.updateConversationNativeDrag(event.clientX, event.clientY);
21471
+ }, true);
21472
+
21473
+ window.cancelConversationGlassFlight = function() {
21474
+ conversationGlassFlight.generation += 1;
21475
+ if (conversationGlassFlight.timer) clearTimeout(conversationGlassFlight.timer);
21476
+ conversationGlassFlight.timer = 0;
21477
+ if (conversationGlassFlight.source) conversationGlassFlight.source.classList.remove('liquid-selection-source');
21478
+ conversationGlassFlight.source = null;
21479
+ if (conversationGlassFlight.covered) conversationGlassFlight.covered.classList.remove('liquid-selection-covered');
21480
+ conversationGlassFlight.covered = null;
21481
+ stopLiquidMotionTracking(conversationGlassFlight.float);
21482
+ if (conversationGlassFlight.float && conversationGlassFlight.float._releaseKyantGlass) {
21483
+ conversationGlassFlight.float._releaseKyantGlass();
21484
+ }
21485
+ if (conversationGlassFlight.float && conversationGlassFlight.float.parentNode) {
21486
+ conversationGlassFlight.float.parentNode.removeChild(conversationGlassFlight.float);
21487
+ }
21488
+ conversationGlassFlight.float = null;
21489
+ if (conversationNativeDrag.row) conversationNativeDrag.row.classList.remove('dragging', 'conversation-glass-dragging');
21490
+ conversationNativeDrag.row = null;
21491
+ conversationNativeDrag.float = null;
21492
+ conversationNativeDrag.arrived = false;
21493
+ window.setLiquidSidebarGestureLock('conversation-flight', false);
21494
+ };
21495
+
21496
+ window.animateConversationGlassTo = function(target, options) {
21497
+ options = options || {};
21498
+ var list = els['conversation-list'];
21499
+ if (!list || !target) { if (options.onArrive) options.onArrive(); return; }
21500
+ var source = list.querySelector('.conv-item.active') || target;
21501
+ var float = conversationGlassFlight.float;
21502
+ var redirecting = !!(float && float.isConnected);
21503
+ if (!redirecting) {
21504
+ float = document.createElement('div');
21505
+ float.className = 'liquid-selection-float conversation-selection-float liquid-selection-primed';
21506
+ mountLiquidViewportFloat(float);
21507
+ captureLiquidBackdropFrame().then(function(frame) {
21508
+ if (float && float.isConnected) attachKyantLiquidRenderer(float, frame);
21509
+ });
21510
+ conversationGlassFlight.float = float;
21511
+ conversationGlassFlight.source = source;
21512
+ source.classList.add('liquid-selection-source');
21513
+ }
21514
+ function place(row) {
21515
+ if (conversationGlassFlight.covered && conversationGlassFlight.covered !== row) {
21516
+ conversationGlassFlight.covered.classList.remove('liquid-selection-covered');
21517
+ }
21518
+ conversationGlassFlight.covered = row;
21519
+ row.classList.add('liquid-selection-covered');
21520
+ positionConversationGlassAroundRow(float, row);
21521
+ }
21522
+ var generation = ++conversationGlassFlight.generation;
21523
+ if (!redirecting) {
21524
+ place(source);
21525
+ showLiquidViewportFloat(float);
21526
+ place(source);
21527
+ // Lock the true source geometry before enabling lift and movement together.
21528
+ float.getBoundingClientRect();
21529
+ requestAnimationFrame(function() {
21530
+ if (conversationGlassFlight.generation !== generation || !float.isConnected) return;
21531
+ float.classList.remove('liquid-selection-primed');
21532
+ float.classList.add('visible');
21533
+ place(target);
21534
+ });
21535
+ window.setLiquidSidebarGestureLock('conversation-flight', true);
21536
+ } else {
21537
+ // Updating top/left while the transition is running makes CSS continue
21538
+ // from the currently rendered position; no second glass block is born.
21539
+ float.classList.remove('liquid-selection-landing');
21540
+ place(target);
21541
+ }
21542
+ if (conversationGlassFlight.timer) clearTimeout(conversationGlassFlight.timer);
21543
+ waitForLiquidSelectionArrival(float, function() {
21544
+ if (conversationGlassFlight.generation !== generation) return;
21545
+ conversationGlassFlight.timer = 0;
21546
+ if (options.dragging && target.isConnected) {
21547
+ wrapConversationDragContent(target, float);
21548
+ window.setLiquidSidebarGestureLock('conversation-flight', false);
21549
+ if (options.onArrive) options.onArrive();
21550
+ return;
21551
+ }
21552
+ landLiquidSelectionFloat(float, function() {
21553
+ if (conversationGlassFlight.generation !== generation) return;
21554
+ if (float._releaseKyantGlass) float._releaseKyantGlass();
21555
+ if (float.parentNode) float.parentNode.removeChild(float);
21556
+ conversationGlassFlight.float = null;
21557
+ if (conversationGlassFlight.source) conversationGlassFlight.source.classList.remove('liquid-selection-source');
21558
+ conversationGlassFlight.source = null;
21559
+ if (conversationGlassFlight.covered) conversationGlassFlight.covered.classList.remove('liquid-selection-covered');
21560
+ conversationGlassFlight.covered = null;
21561
+ window.setLiquidSidebarGestureLock('conversation-flight', false);
21562
+ if (options.onArrive) options.onArrive();
21563
+ });
21564
+ });
21565
+ };
21566
+
21567
+ window.activateConversationWithGlass = function(idx, row) {
21568
+ var conversations = currentWorkspaceConversations();
21569
+ if (!row || !conversations[idx]) {
21570
+ window.switchConversation(idx);
21571
+ return;
21572
+ }
21573
+ var targetId = String(conversations[idx].id || '');
21574
+ window.animateConversationGlassTo(row, { onArrive: function() {
21575
+ var current = currentWorkspaceConversations();
21576
+ var targetIndex = current.findIndex(function(item) { return String(item.id || '') === targetId; });
21577
+ if (targetIndex >= 0) window.switchConversation(targetIndex);
21578
+ } });
21579
+ };
21580
+
19567
21581
  function renderConversations() {
19568
21582
  var list = els['conversation-list'];
19569
21583
  if (!list) return;
19570
- list.innerHTML = '';
21584
+ if (list.dataset.conversationDropWired !== 'true') {
21585
+ list.dataset.conversationDropWired = 'true';
21586
+ list.addEventListener('dragover', function(event) {
21587
+ window.updateConversationNativeDrag(event.clientX, event.clientY);
21588
+ if (conversationNativeDrag.row) event.preventDefault();
21589
+ });
21590
+ list.addEventListener('drop', function(event) {
21591
+ if (!list.querySelector('.conv-item.dragging')) return;
21592
+ event.preventDefault();
21593
+ list.dataset.conversationDropCommitted = 'true';
21594
+ window.commitConversationDomOrder();
21595
+ });
21596
+ }
19571
21597
  if (!state.currentWorkspace) {
19572
21598
  list.innerHTML = '<div style="padding:14px 10px;font-size:12px;color:var(--text-dim);line-height:1.5;text-align:center;">' + esc(t('workspace.selectFirst')) + '</div>';
19573
21599
  updateWorkspaceGate();
19574
21600
  return;
19575
21601
  }
19576
21602
  var conversations = currentWorkspaceConversations();
21603
+ // 工作区对话被清空后保持为空:只显示空态提示,不自动创建对话。
21604
+ if (!conversations.length) {
21605
+ list.innerHTML = '<div style="padding:14px 10px;font-size:12px;color:var(--text-dim);line-height:1.5;text-align:center;">' + esc(t('conversation.emptyWorkspace')) + '</div>';
21606
+ updateWorkspaceGate();
21607
+ return;
21608
+ }
19577
21609
  var summaryCounts = {};
19578
21610
  for (var ci = 0; ci < conversations.length; ci++) {
19579
21611
  var summaryKey = String(conversations[ci] && conversations[ci].summary || '');
@@ -19582,7 +21614,17 @@ function renderConversations() {
19582
21614
  var activeBeforeRender = (conversations.find(function(c) { return c && c.active; }) || {}).id;
19583
21615
  if (activeBeforeRender) setActiveWorkspaceConversationById(activeBeforeRender);
19584
21616
  else setActiveWorkspaceConversation(state.workspaceActiveConversation[currentWorkspaceKey()] || 0);
19585
- var conversationNodes = [];
21617
+ // 增量更新:复用已存在的行节点,就地更新 class 与内容。行节点不被替换,
21618
+ // 运行中对话的 marquee 跑马灯 CSS 动画就不会因重建 DOM 而从头开始。
21619
+ Array.prototype.slice.call(list.children).forEach(function(child) {
21620
+ if (!child || !child.classList || !child.classList.contains('conv-item')) child.parentNode.removeChild(child);
21621
+ });
21622
+ var existingRows = {};
21623
+ Array.prototype.slice.call(list.querySelectorAll('.conv-item')).forEach(function(row) {
21624
+ existingRows[String(row.getAttribute('data-conversation-id') || '')] = row;
21625
+ });
21626
+ var keepRows = {};
21627
+ var orderedRows = [];
19586
21628
  for (var i = 0; i < conversations.length; i++) {
19587
21629
  var conv = conversations[i];
19588
21630
  var runtimeKey = currentRuntimeKey(conv.id);
@@ -19591,53 +21633,114 @@ function renderConversations() {
19591
21633
  if (summaryCounts[String(conv.summary || '')] > 1) {
19592
21634
  displaySummary += ' · ' + String(conv.id || '').slice(-8);
19593
21635
  }
19594
- var div = document.createElement('div');
19595
- div.className = 'conv-item' + (conv.active ? ' active' : '');
19596
- div.setAttribute('role', 'listitem');
19597
- div.setAttribute('tabindex', conv.active ? '0' : '-1');
19598
- div.setAttribute('aria-current', conv.active ? 'true' : 'false');
19599
- div.setAttribute('draggable', 'true');
19600
- div.setAttribute('data-conversation-id', String(conv.id || ''));
19601
- if (runtimeState && ['running', 'stopping', 'force_restarting'].indexOf(String(runtimeState.status || '')) >= 0) div.classList.add('marquee-border');
21636
+ var running = !!(runtimeState && ['running', 'stopping', 'force_restarting'].indexOf(String(runtimeState.status || '')) >= 0);
19602
21637
  var runtimeBadge = runtimeState && runtimeState.status && runtimeState.status !== 'idle'
19603
21638
  ? '<span class="conv-runtime-badge ' + escAttr(String(runtimeState.status)) + '">' + esc(String(runtimeState.status)) + '</span>' : '';
19604
21639
  var branchCommIcon = conv.branchCommunication
19605
21640
  ? '<span class="conv-branch-comm-icon" title="' + escAttr(t('conversation.branchCommunicationBadge')) + '" aria-label="' + escAttr(t('conversation.branchCommunicationBadge')) + '">' + iconSvg('git-branch', t('conversation.branchCommunicationBadge')) + '</span>' : '';
19606
- div.innerHTML = '<span class="conv-summary" title="' + escAttr(String(conv.id || '')) + '">' + esc(displaySummary) + (conv.messageCount ? ' (' + esc(String(conv.messageCount)) + ')' : '') + '</span>' + branchCommIcon + runtimeBadge +
21641
+ var innerKey = [String(displaySummary), String(conv.messageCount || ''), String(runtimeState && runtimeState.status || ''), conv.branchCommunication ? '1' : '0'].join('|');
21642
+ var nextInner = '<span class="conv-summary" title="' + escAttr(String(conv.id || '')) + '">' + esc(displaySummary) + (conv.messageCount ? ' (' + esc(String(conv.messageCount)) + ')' : '') + '</span>' + branchCommIcon + runtimeBadge +
19607
21643
  '<button class="conv-more-btn" aria-haspopup="menu" aria-expanded="false" onclick="event.stopPropagation();window.openConversationActionMenu(this, this.closest(&quot;.conv-item&quot;).getAttribute(&quot;data-conversation-id&quot;))" title="' + escAttr(t('conversation.more')) + '">' + iconOnly('ellipsis', t('conversation.more')) + '</button>';
19608
- div.onclick = function(idx) { return function() { window.switchConversation(idx); }; }(i);
19609
- div.addEventListener('keydown', window.handleConversationKey);
19610
- div.addEventListener('contextmenu', function(event) {
19611
- event.preventDefault();
19612
- event.stopPropagation();
19613
- window.openConversationActionMenu(event.currentTarget, this.getAttribute('data-conversation-id') || '', { x: event.clientX, y: event.clientY });
19614
- });
19615
- div.addEventListener('dragstart', function(event) {
19616
- this.classList.add('dragging');
19617
- if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', this.getAttribute('data-conversation-id') || ''); }
19618
- });
19619
- div.addEventListener('dragend', function() {
19620
- this.classList.remove('dragging');
19621
- Array.from(list.querySelectorAll('.conv-item')).forEach(function(row) { row.classList.remove('drag-over'); });
19622
- });
19623
- div.addEventListener('dragover', function(event) {
19624
- var dragging = list.querySelector('.conv-item.dragging');
19625
- if (!dragging || dragging === this) return;
19626
- var draggingConv = conversations.find(function(item) { return String(item.id) === String(dragging.getAttribute('data-conversation-id')); });
19627
- var targetConv = conversations.find(function(item) { return String(item.id) === String(this.getAttribute('data-conversation-id')); }, this);
19628
- if (!draggingConv || !targetConv || !!draggingConv.pinned !== !!targetConv.pinned) return;
19629
- event.preventDefault();
19630
- this.classList.add('drag-over');
19631
- });
19632
- div.addEventListener('dragleave', function() { this.classList.remove('drag-over'); });
19633
- div.addEventListener('drop', function(event) {
19634
- event.preventDefault();
19635
- this.classList.remove('drag-over');
19636
- window.reorderConversationBefore(event.dataTransfer ? event.dataTransfer.getData('text/plain') : '', this.getAttribute('data-conversation-id') || '');
19637
- });
19638
- conversationNodes.push(div);
21644
+ var div = existingRows[String(conv.id || '')];
21645
+ if (div) {
21646
+ keepRows[String(conv.id || '')] = true;
21647
+ } else {
21648
+ div = document.createElement('div');
21649
+ div.setAttribute('role', 'listitem');
21650
+ div.setAttribute('draggable', 'true');
21651
+ div.setAttribute('data-conversation-id', String(conv.id || ''));
21652
+ div.addEventListener('keydown', window.handleConversationKey);
21653
+ div.addEventListener('contextmenu', function(event) {
21654
+ event.preventDefault();
21655
+ event.stopPropagation();
21656
+ window.openConversationActionMenu(event.currentTarget, this.getAttribute('data-conversation-id') || '', { x: event.clientX, y: event.clientY });
21657
+ });
21658
+ div.addEventListener('dragstart', function(event) {
21659
+ window.cancelConversationGlassFlight();
21660
+ var rect = this.getBoundingClientRect();
21661
+ conversationNativeDrag.row = this;
21662
+ conversationNativeDrag.arrived = false;
21663
+ conversationNativeDrag.grabX = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
21664
+ conversationNativeDrag.grabY = Math.max(0, Math.min(rect.height, event.clientY - rect.top));
21665
+ conversationNativeDrag.clientX = event.clientX;
21666
+ conversationNativeDrag.clientY = event.clientY;
21667
+ if (event.dataTransfer) {
21668
+ var transparentDragImage = document.createElement('canvas');
21669
+ transparentDragImage.width = transparentDragImage.height = 1;
21670
+ document.body.appendChild(transparentDragImage);
21671
+ event.dataTransfer.setDragImage(transparentDragImage, 0, 0);
21672
+ setTimeout(function() { transparentDragImage.remove(); }, 0);
21673
+ }
21674
+ list.classList.add('conversation-reordering');
21675
+ list.dataset.conversationDropCommitted = 'false';
21676
+ window.setLiquidSidebarGestureLock('conversation-reorder', true);
21677
+ window.animateConversationGlassTo(this, { dragging: true });
21678
+ if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', this.getAttribute('data-conversation-id') || ''); }
21679
+ });
21680
+ div.addEventListener('drag', function(event) {
21681
+ if (conversationNativeDrag.row === this && (event.clientX || event.clientY)) {
21682
+ window.updateConversationNativeDrag(event.clientX, event.clientY);
21683
+ }
21684
+ });
21685
+ div.addEventListener('dragend', function() {
21686
+ var releasedRow = this;
21687
+ list.classList.remove('conversation-reordering');
21688
+ Array.from(list.querySelectorAll('.conv-item')).forEach(function(row) { row.classList.remove('drag-over'); });
21689
+ window.animateConversationGlassTo(releasedRow, { onArrive: function() {
21690
+ releasedRow.classList.remove('dragging', 'conversation-glass-dragging');
21691
+ conversationNativeDrag.row = null;
21692
+ conversationNativeDrag.float = null;
21693
+ conversationNativeDrag.arrived = false;
21694
+ window.setLiquidSidebarGestureLock('conversation-reorder', false);
21695
+ renderConversations();
21696
+ } });
21697
+ });
21698
+ div.addEventListener('dragover', function(event) {
21699
+ window.updateConversationNativeDrag(event.clientX, event.clientY);
21700
+ var dragging = list.querySelector('.conv-item.dragging');
21701
+ if (!dragging || dragging === this) return;
21702
+ var convsNow = currentWorkspaceConversations();
21703
+ var draggingConv = convsNow.find(function(item) { return String(item.id) === String(dragging.getAttribute('data-conversation-id')); });
21704
+ var targetConv = convsNow.find(function(item) { return String(item.id) === String(this.getAttribute('data-conversation-id')); }, this);
21705
+ if (!draggingConv || !targetConv || !!draggingConv.pinned !== !!targetConv.pinned) return;
21706
+ event.preventDefault();
21707
+ var targetRect = this.getBoundingClientRect();
21708
+ var after = event.clientY >= targetRect.top + targetRect.height / 2;
21709
+ var anchor = after ? this.nextSibling : this;
21710
+ if (anchor !== dragging) list.insertBefore(dragging, anchor);
21711
+ Array.from(list.querySelectorAll('.conv-item')).forEach(function(row) { row.classList.remove('drag-over'); });
21712
+ });
21713
+ div.addEventListener('dragleave', function() { this.classList.remove('drag-over'); });
21714
+ div.addEventListener('drop', function(event) {
21715
+ event.preventDefault();
21716
+ event.stopPropagation();
21717
+ this.classList.remove('drag-over');
21718
+ list.dataset.conversationDropCommitted = 'true';
21719
+ window.commitConversationDomOrder();
21720
+ });
21721
+ existingRows[String(conv.id || '')] = div;
21722
+ }
21723
+ div.className = 'conv-item' + (conv.active ? ' active' : '');
21724
+ if (running) div.classList.add('marquee-border');
21725
+ div.setAttribute('tabindex', conv.active ? '0' : '-1');
21726
+ div.setAttribute('aria-current', conv.active ? 'true' : 'false');
21727
+ if (div.getAttribute('data-inner-key') !== innerKey) {
21728
+ div.setAttribute('data-inner-key', innerKey);
21729
+ div.innerHTML = nextInner;
21730
+ }
21731
+ div.onclick = function(idx) { return function(event) { window.activateConversationWithGlass(idx, event.currentTarget); }; }(i);
21732
+ orderedRows.push(div);
19639
21733
  }
19640
- appendDomNodesInBatches(list, conversationNodes, 'conversation-list', 24);
21734
+ Object.keys(existingRows).forEach(function(id) {
21735
+ if (keepRows[id]) return;
21736
+ var stale = existingRows[id];
21737
+ if (stale && stale.parentNode === list) stale.parentNode.removeChild(stale);
21738
+ });
21739
+ orderedRows.forEach(function(row, idx) {
21740
+ var current = list.children[idx];
21741
+ if (current === row) return;
21742
+ list.insertBefore(row, current || null);
21743
+ });
19641
21744
  updateWorkspaceGate();
19642
21745
  }
19643
21746
 
@@ -19925,16 +22028,28 @@ window.archiveConv = function(conversationId) {
19925
22028
  var currentConvs = currentWorkspaceConversations();
19926
22029
  var currentIdx = currentConvs.findIndex(function(item) { return String(item && item.id || 'default') === targetId; });
19927
22030
  if (currentIdx >= 0) currentConvs.splice(currentIdx, 1);
19928
- if (!currentConvs.length) currentConvs.push({ id: 'default', summary: t('workspace.defaultConversation'), archived: false, active: true });
19929
- var nextActiveId = priorActiveId && priorActiveId !== targetId ? priorActiveId : String(currentConvs[Math.min(Math.max(currentIdx, 0), currentConvs.length - 1)].id || 'default');
19930
- var nextActiveIndex = Math.max(0, currentConvs.findIndex(function(item) { return String(item && item.id || '') === nextActiveId; }));
19931
- for (var i = 0; i < currentConvs.length; i++) currentConvs[i].active = i === nextActiveIndex;
19932
- state.workspaceActiveConversation[workspaceKey] = nextActiveIndex;
19933
- state.activeConversation = nextActiveIndex;
19934
- state.conversations = currentConvs;
19935
- if (priorActiveId === targetId) {
19936
- state.activeBackendConversationId = nextActiveId;
19937
- if (els['chat-area']) els['chat-area'].innerHTML = '';
22031
+ if (!currentConvs.length) {
22032
+ // 工作区所有对话被清空后保持为空:不再自动创建 default 对话。
22033
+ for (var emptyI = 0; emptyI < currentConvs.length; emptyI++) currentConvs[emptyI].active = false;
22034
+ state.workspaceActiveConversation[workspaceKey] = -1;
22035
+ state.activeConversation = -1;
22036
+ state.conversations = currentConvs;
22037
+ if (priorActiveId === targetId) {
22038
+ state.activeBackendConversationId = '';
22039
+ if (els['chat-area']) els['chat-area'].innerHTML = '';
22040
+ if (window.renderFlowTakeover) window.renderFlowTakeover(false, '', { target: targetRuntime });
22041
+ }
22042
+ } else {
22043
+ var nextActiveId = priorActiveId && priorActiveId !== targetId ? priorActiveId : String(currentConvs[Math.min(Math.max(currentIdx, 0), currentConvs.length - 1)].id || 'default');
22044
+ var nextActiveIndex = Math.max(0, currentConvs.findIndex(function(item) { return String(item && item.id || '') === nextActiveId; }));
22045
+ for (var i = 0; i < currentConvs.length; i++) currentConvs[i].active = i === nextActiveIndex;
22046
+ state.workspaceActiveConversation[workspaceKey] = nextActiveIndex;
22047
+ state.activeConversation = nextActiveIndex;
22048
+ state.conversations = currentConvs;
22049
+ if (priorActiveId === targetId) {
22050
+ state.activeBackendConversationId = nextActiveId;
22051
+ if (els['chat-area']) els['chat-area'].innerHTML = '';
22052
+ }
19938
22053
  }
19939
22054
  if (state.flowTakeovers) {
19940
22055
  var archivedFlowKey = runtimeKeyFor(targetRuntime.workspaceId, targetRuntime.conversationId);
@@ -20138,6 +22253,7 @@ window.renderMemoryLab = function() {
20138
22253
  html += '</div>';
20139
22254
  panel.innerHTML = html;
20140
22255
  if (state.memoryLabSearchQuery) window.updateMemoryLabSearch(state.memoryLabSearchQuery);
22256
+ wireLiquidRailInteractions(panel.querySelector('.memory-lab-view-menu'), 'button[data-memory-view]');
20141
22257
  if (state.memoryLabView === 'overview') requestAnimationFrame(function() { window.initMemoryLabOverview(); });
20142
22258
  state.memoryLabNavDirection = 'fade';
20143
22259
  };
@@ -20146,8 +22262,8 @@ window.memoryLabTopbar = function(lab) {
20146
22262
  var view = state.memoryLabView || 'overview';
20147
22263
  return '<div class="memory-lab-panel"><div class="memory-lab-topbar">' +
20148
22264
  '<div class="memory-lab-view-menu" role="tablist">' +
20149
- '<button class="' + (view === 'overview' ? 'active' : '') + '" onclick="window.switchMemoryLabView(\'overview\')">' + esc(t('memoryLab.overview')) + '</button>' +
20150
- '<button class="' + (view === 'detail' ? 'active' : '') + '" onclick="window.switchMemoryLabView(\'detail\')">' + esc(t('memoryLab.detail')) + '</button>' +
22265
+ '<button data-memory-view="overview" class="' + (view === 'overview' ? 'active' : '') + '" onclick="window.switchMemoryLabView(\'overview\')">' + esc(t('memoryLab.overview')) + '</button>' +
22266
+ '<button data-memory-view="detail" class="' + (view === 'detail' ? 'active' : '') + '" onclick="window.switchMemoryLabView(\'detail\')">' + esc(t('memoryLab.detail')) + '</button>' +
20151
22267
  '</div>' +
20152
22268
  '<div class="memory-lab-search">' +
20153
22269
  '<input id="memory-lab-search-input" value="' + escAttr(state.memoryLabSearchQuery || '') + '" placeholder="' + escAttr(t('memoryLab.search')) + '" oninput="window.updateMemoryLabSearch(this.value)" onfocus="window.updateMemoryLabSearch(this.value)" />' +
@@ -20885,6 +23001,7 @@ window.showPluginList = function(tab, options) {
20885
23001
  state.activeSubWindowView = { name: 'plugins', tab: requested };
20886
23002
  }
20887
23003
  window.syncPluginTabs(!pluginOpen || !!(options && options.focusTab));
23004
+ wireLiquidRailInteractions(document.querySelector('.plugin-tabs[role="tablist"]'), '.stab-btn[data-plugin-tab]');
20888
23005
  var panel = document.getElementById('plugin-panel');
20889
23006
  if (panel) panel.replaceChildren();
20890
23007
  if (requested === 'mcp') window.renderMcpManager();
@@ -21798,6 +23915,30 @@ window.renderLeftWsList = function() {
21798
23915
  var holder = document.createElement('div');
21799
23916
  holder.innerHTML = html;
21800
23917
  appendDomNodesInBatches(container, Array.from(holder.children), 'workspace-list', 24);
23918
+ wireLiquidRailInteractions(container, '.left-ws-item');
23919
+ };
23920
+
23921
+ window.commitConversationDomOrder = function() {
23922
+ var list = els['conversation-list'];
23923
+ var convs = currentWorkspaceConversations();
23924
+ if (!list || !convs.length) return;
23925
+ window.cancelConversationGlassFlight();
23926
+ var byId = {};
23927
+ convs.forEach(function(item) { byId[String(item.id)] = item; });
23928
+ var ids = Array.from(list.querySelectorAll('.conv-item')).map(function(row) {
23929
+ return String(row.getAttribute('data-conversation-id') || '');
23930
+ }).filter(function(id) { return !!byId[id]; });
23931
+ if (ids.length !== convs.length) return renderConversations();
23932
+ var activeId = activeConversationId();
23933
+ convs.splice.apply(convs, [0, convs.length].concat(ids.map(function(id) { return byId[id]; })));
23934
+ setActiveWorkspaceConversationById(activeId);
23935
+ renderConversations();
23936
+ Promise.resolve(api.reorderConversations && api.reorderConversations(ids)).then(function(saved) {
23937
+ if (!saved) throw new Error(t('conversation.reorderFailed'));
23938
+ }).catch(function() {
23939
+ showUiNotice(t('conversation.reorderFailed'), 'error', 'reorder-conversations');
23940
+ if (api.getState) api.getState(currentConversationTarget(activeId)).then(function(s) { if (s) applyWorkspaceStateFromBackend(s); });
23941
+ });
21801
23942
  };
21802
23943
 
21803
23944
  window.handleWorkspaceKey = function(event) {
@@ -21916,10 +24057,10 @@ window.showNewWorkspaceDialog = function() {
21916
24057
  state.activeSubWindowView = { name: 'workspaceNew' };
21917
24058
  var html = '<div style="padding:10px;">' +
21918
24059
  '<label style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:6px;">' + esc(t('workspace.type')) + '</label>' +
21919
- '<div style="display:flex;gap:10px;margin-bottom:12px;">' +
21920
- '<label style="display:flex;align-items:center;gap:4px;font-size:12px;cursor:pointer;"><input type="radio" name="ws-type" value="internal" checked onchange="window.toggleWsType()"> ' + esc(t('workspace.internal')) + '</label>' +
21921
- '<label style="display:flex;align-items:center;gap:4px;font-size:12px;cursor:pointer;"><input type="radio" name="ws-type" value="external" onchange="window.toggleWsType()"> ' + esc(t('workspace.external')) + '</label>' +
21922
- '<label style="display:flex;align-items:center;gap:4px;font-size:12px;cursor:pointer;"><input type="radio" name="ws-type" value="ssh" onchange="window.toggleWsType()"> ' + esc(t('workspace.ssh')) + '</label>' +
24060
+ '<div class="workspace-type-pager" role="radiogroup" aria-label="' + escAttr(t('workspace.type')) + '">' +
24061
+ '<label class="workspace-type-option active"><input type="radio" name="ws-type" value="internal" checked onchange="window.toggleWsType()">' + esc(t('workspace.internal')) + '</label>' +
24062
+ '<label class="workspace-type-option"><input type="radio" name="ws-type" value="external" onchange="window.toggleWsType()">' + esc(t('workspace.external')) + '</label>' +
24063
+ '<label class="workspace-type-option"><input type="radio" name="ws-type" value="ssh" onchange="window.toggleWsType()">' + esc(t('workspace.ssh')) + '</label>' +
21923
24064
  '</div>' +
21924
24065
  '<label id="ws-name-label-dlg" style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:8px;">' + esc(t('workspace.name')) + '</label>' +
21925
24066
  '<input id="new-ws-input" style="width:100%;padding:8px 10px;border:1px solid var(--glass-border-2);border-radius:var(--radius-sm);background:var(--glass-bg-1);color:var(--text);font-size:13px;outline:none;box-sizing:border-box;" placeholder="' + escAttr(t('workspace.enterName')) + '" onkeydown="if(event.key===\'Enter\'&&!event.isComposing&&event.key!==\'Process\'&&event.keyCode!==229)window.doCreateWorkspace()">' +
@@ -21944,6 +24085,7 @@ window.showNewWorkspaceDialog = function() {
21944
24085
  '<button class="sec-btn" onclick="window.closeSubWin()">' + esc(t('common.cancel')) + '</button>' +
21945
24086
  '</div></div>';
21946
24087
  window.openSubWin(t('workspace.new'), html);
24088
+ wireLiquidRailInteractions(document.querySelector('.workspace-type-pager'), '.workspace-type-option');
21947
24089
  setTimeout(function() {
21948
24090
  var inp = document.getElementById('new-ws-input');
21949
24091
  if (inp) inp.focus();
@@ -21955,6 +24097,11 @@ window.toggleWsType = function() {
21955
24097
  for (var i = 0; i < radios.length; i++) {
21956
24098
  if (radios[i].checked) { selected = radios[i].value; break; }
21957
24099
  }
24100
+ var options = document.querySelectorAll('.workspace-type-option');
24101
+ for (var oi = 0; oi < options.length; oi++) {
24102
+ var radio = options[oi].querySelector('input[name="ws-type"]');
24103
+ options[oi].classList.toggle('active', !!(radio && radio.checked));
24104
+ }
21958
24105
  var extPath = document.getElementById('ws-ext-path');
21959
24106
  var sshFields = document.getElementById('ws-ssh-fields');
21960
24107
  var nameLabel = document.getElementById('ws-name-label-dlg');
@@ -23058,6 +25205,7 @@ function schedulePostStartupUiRendering() {
23058
25205
  this.setAttribute('aria-valuenow', String(Math.round(size)));
23059
25206
  });
23060
25207
  handles[i].addEventListener('mousedown', function(e) {
25208
+ if (window.liquidSidebarGesturesLocked && window.liquidSidebarGesturesLocked()) return;
23061
25209
  e.preventDefault();
23062
25210
  var handle = this;
23063
25211
  var target = this.getAttribute('data-target');