newmark-agent 0.5.4 → 0.5.8

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 {
@@ -2460,9 +2537,12 @@ button.left-ws-item {
2460
2537
  text-align: left;
2461
2538
  cursor: pointer;
2462
2539
  font: 600 12px var(--font);
2540
+ transition: none !important;
2541
+ animation: none !important;
2542
+ transform: none !important;
2463
2543
  }
2464
2544
 
2465
- .conversation-work-run-head:hover { color: var(--text-bright); }
2545
+ .conversation-work-run-head:hover { color: var(--text-bright); filter: none; }
2466
2546
 
2467
2547
  .conversation-work-run-chevron {
2468
2548
  width: 8px;
@@ -2471,7 +2551,7 @@ button.left-ws-item {
2471
2551
  border-right: 1px solid var(--text-dim);
2472
2552
  border-bottom: 1px solid var(--text-dim);
2473
2553
  transform: rotate(45deg);
2474
- transition: transform 150ms ease;
2554
+ transition: none;
2475
2555
  }
2476
2556
 
2477
2557
  .conversation-work-run.collapsed .conversation-work-run-chevron { transform: rotate(-45deg); }
@@ -3296,8 +3376,7 @@ button.todo-item { background: transparent; }
3296
3376
  padding: 12px 16px;
3297
3377
  border-top: 1px solid var(--glass-border-2);
3298
3378
  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%);
3379
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow);
3301
3380
  }
3302
3381
 
3303
3382
  #input-area textarea {
@@ -3443,10 +3522,9 @@ button.todo-item { background: transparent; }
3443
3522
  overflow: auto;
3444
3523
  padding: 7px;
3445
3524
  border: 1px solid var(--glass-border-2);
3446
- border-radius: var(--radius-lg);
3525
+ border-radius: var(--radius-xl);
3447
3526
  background: var(--modal-surface);
3448
- box-shadow: var(--shadow-lg);
3449
- backdrop-filter: blur(var(--glass-blur-3)) saturate(1.08);
3527
+ box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
3450
3528
  }
3451
3529
  .model-select-menu:popover-open { display: block; animation: model-menu-in 150ms var(--ease-out-expo); }
3452
3530
 
@@ -3470,6 +3548,85 @@ button.todo-item { background: transparent; }
3470
3548
  .model-select-menu-option:hover,
3471
3549
  .model-select-menu-option:focus-visible { background: var(--control-hover-bg); color: var(--text-bright); outline: none; }
3472
3550
  .model-select-menu-option.selected { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--text-bright); }
3551
+ .liquid-selection-source { background: transparent !important; border-color: transparent !important; }
3552
+ .liquid-selection-source::after { display: none !important; opacity: 0 !important; }
3553
+ .conv-item.liquid-selection-covered { border-color: transparent !important; }
3554
+ .conv-item.liquid-selection-source.marquee-border::before,
3555
+ .conv-item.liquid-selection-covered.marquee-border::before { opacity: 0 !important; }
3556
+ /* Rails and selectable menus are governed by one shared moving glass block.
3557
+ Suppress the generic pressed pseudo-glass so the destination never creates
3558
+ a second block while the source block is flying toward it. */
3559
+ .model-select-menu-option::after,
3560
+ .newmark-select-option::after,
3561
+ .left-nav-icon::after,
3562
+ .lt-item::after,
3563
+ .left-ws-item::after,
3564
+ .tab-btn::after,
3565
+ .stab-btn[data-stab]::after,
3566
+ .stab-btn[data-plugin-tab]::after,
3567
+ .mode-toggle-btn[data-mode]::after,
3568
+ .memory-lab-view-menu button[data-memory-view]::after {
3569
+ display: none !important;
3570
+ opacity: 0 !important;
3571
+ }
3572
+ .left-ws-item.liquid-selection-source,
3573
+ .tab-btn.liquid-selection-source { background: transparent !important; }
3574
+ #left-ws-list,
3575
+ #right-tabs { position: relative; }
3576
+ .left-nav-icon.liquid-tool-active,
3577
+ .lt-item.liquid-tool-active { background: var(--control-hover-bg); color: var(--text-bright); }
3578
+ .liquid-selection-float {
3579
+ --liquid-lift-scale: .94;
3580
+ --liquid-motion-angle: 0rad;
3581
+ --liquid-motion-stretch: 1;
3582
+ --liquid-motion-squash: 1;
3583
+ position: absolute;
3584
+ z-index: 4;
3585
+ display: flex;
3586
+ align-items: center;
3587
+ padding: 6px 10px;
3588
+ box-sizing: border-box;
3589
+ border-radius: var(--radius-full);
3590
+ color: var(--text-bright);
3591
+ pointer-events: none;
3592
+ overflow: visible;
3593
+ opacity: 0;
3594
+ 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)));
3595
+ transform-origin: center;
3596
+ background: transparent;
3597
+ background-color: transparent !important;
3598
+ background-image: none !important;
3599
+ backdrop-filter: none;
3600
+ -webkit-backdrop-filter: none;
3601
+ 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);
3602
+ 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;
3603
+ will-change: top, left, width, height, transform;
3604
+ }
3605
+ .liquid-selection-float.visible { opacity: 1; --liquid-lift-scale: 1; }
3606
+ .liquid-selection-float.liquid-selection-landing {
3607
+ opacity: 0;
3608
+ --liquid-lift-scale: .86;
3609
+ 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)));
3610
+ transition: opacity 120ms var(--ease-out-expo), transform 120ms var(--ease-out-expo) !important;
3611
+ }
3612
+ .liquid-selection-float.liquid-selection-viewport {
3613
+ position: fixed;
3614
+ inset: auto;
3615
+ z-index: 1000;
3616
+ margin: 0;
3617
+ border: 0;
3618
+ }
3619
+ .liquid-selection-float.liquid-selection-primed { transition: none !important; }
3620
+ .liquid-selection-float.liquid-selection-viewport:popover-open { display: flex; }
3621
+ .liquid-selection-float::-webkit-scrollbar { display: none; }
3622
+ .liquid-selection-canvas {
3623
+ position: absolute;
3624
+ inset: 0;
3625
+ width: 100%;
3626
+ height: 100%;
3627
+ border-radius: inherit;
3628
+ pointer-events: none;
3629
+ }
3473
3630
  @keyframes model-menu-in { from { opacity: 0; transform: translateY(5px) scale(.985); } to { opacity: 1; transform: none; } }
3474
3631
 
3475
3632
  .newmark-select-shell {
@@ -3508,8 +3665,7 @@ button.todo-item { background: transparent; }
3508
3665
  border: 1px solid var(--glass-border-2);
3509
3666
  border-radius: var(--radius-lg);
3510
3667
  background: var(--modal-surface);
3511
- box-shadow: var(--shadow-lg);
3512
- backdrop-filter: blur(var(--glass-blur-3)) saturate(1.08);
3668
+ box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
3513
3669
  }
3514
3670
  .newmark-select-shell.open .newmark-select-menu { display: block; animation: model-menu-in 150ms var(--ease-out-expo); }
3515
3671
  .newmark-select-menu-group { padding: 7px 9px 4px; color: var(--text-accent); font: 700 11px var(--font-ui); }
@@ -3530,6 +3686,7 @@ button.todo-item { background: transparent; }
3530
3686
  .newmark-select-option:hover,
3531
3687
  .newmark-select-option:focus-visible { background: var(--control-hover-bg); color: var(--text-bright); outline: none; }
3532
3688
  .newmark-select-option.selected { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--text-bright); }
3689
+ .newmark-select-option.liquid-selection-source { background: transparent !important; }
3533
3690
  .newmark-select-option:disabled { opacity: .42; cursor: not-allowed; }
3534
3691
  .setting-control > .newmark-select-shell,
3535
3692
  .auto-input-group > .newmark-select-shell,
@@ -3668,9 +3825,11 @@ button.todo-item { background: transparent; }
3668
3825
  .mode-toggle {
3669
3826
  display: flex;
3670
3827
  border: 1px solid var(--border);
3671
- border-radius: var(--radius-md);
3672
- overflow: hidden;
3828
+ border-radius: var(--radius-full);
3829
+ overflow: visible;
3673
3830
  flex-shrink: 0;
3831
+ position: relative;
3832
+ padding: 2px;
3674
3833
  }
3675
3834
 
3676
3835
  .mode-toggle-btn {
@@ -3686,9 +3845,25 @@ button.todo-item { background: transparent; }
3686
3845
  }
3687
3846
 
3688
3847
  .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); }
3848
+ .mode-toggle-btn.active { background: rgba(91,120,255,0.15); color: var(--accent); border-radius: var(--radius-full); }
3690
3849
  .mode-toggle-btn:active { transform: scale(0.95) translateZ(0); }
3691
3850
 
3851
+ .workspace-type-pager {
3852
+ display: grid;
3853
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3854
+ position: relative;
3855
+ gap: 2px;
3856
+ padding: 3px;
3857
+ margin-bottom: 12px;
3858
+ border: 1px solid var(--border);
3859
+ border-radius: var(--radius-full);
3860
+ overflow: visible;
3861
+ background: var(--control-bg);
3862
+ }
3863
+ .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; }
3864
+ .workspace-type-option.active { background:var(--control-hover-bg); color:var(--text-bright); }
3865
+ .workspace-type-option input { position:absolute; opacity:0; pointer-events:none; }
3866
+
3692
3867
  #submit-btn {
3693
3868
  width: 34px; height: 34px;
3694
3869
  border-radius: 50%;
@@ -3809,12 +3984,12 @@ button.todo-item { background: transparent; }
3809
3984
  linear-gradient(#fff 0 0) content-box,
3810
3985
  linear-gradient(#fff 0 0);
3811
3986
  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;
3987
+ /* Live marquee rotation. The @property-registered --marquee-angle animates
3988
+ the conic-gradient sweep; Chromium promotes the masked layer so the border
3989
+ stays composited. Release smoke tests verify the animation is active while
3990
+ ordinary text input remains responsive (see release-ui-render-performance
3991
+ and release-ui-icon smoke). */
3992
+ animation: marquee-rotate var(--marquee-speed) linear infinite;
3818
3993
  z-index: -1;
3819
3994
  pointer-events: none;
3820
3995
  transform: translateZ(0);
@@ -3825,7 +4000,7 @@ button.todo-item { background: transparent; }
3825
4000
  background: linear-gradient(var(--marquee-angle, 0deg), var(--g1), var(--g2), var(--g3), var(--g4), var(--g1));
3826
4001
  }
3827
4002
  @keyframes marquee-rotate-fallback { 0% { filter: hue-rotate(0deg); } 100% { filter: hue-rotate(360deg); } }
3828
- .marquee-border::before { animation: none; }
4003
+ .marquee-border::before { animation: marquee-rotate-fallback var(--marquee-speed) linear infinite; }
3829
4004
  }
3830
4005
 
3831
4006
  .working-glow {
@@ -4071,16 +4246,24 @@ button.todo-item { background: transparent; }
4071
4246
  ============================================================ */
4072
4247
  .settings-tabs {
4073
4248
  display: flex;
4249
+ position: relative;
4074
4250
  gap: 4px;
4075
- padding-bottom: 14px;
4076
- border-bottom: 1px solid var(--glass-border-1);
4251
+ width: fit-content;
4252
+ max-width: 100%;
4253
+ padding: 4px;
4254
+ border: 1px solid var(--glass-border-1);
4255
+ border-radius: var(--radius-full);
4256
+ background: var(--glass-bg-1);
4077
4257
  margin-bottom: 14px;
4258
+ overflow-x: auto;
4259
+ scrollbar-width: none;
4078
4260
  }
4261
+ .settings-tabs::-webkit-scrollbar { display: none; }
4079
4262
 
4080
4263
  .stab-btn {
4081
4264
  padding: 6px 14px;
4082
4265
  border: 1px solid var(--border);
4083
- border-radius: var(--radius-md);
4266
+ border-radius: var(--radius-full);
4084
4267
  background: transparent;
4085
4268
  color: var(--text-dim);
4086
4269
  font-size: 12px;
@@ -4391,6 +4574,42 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4391
4574
  box-shadow: 0 0 4px rgba(0,0,0,0.3);
4392
4575
  }
4393
4576
 
4577
+ #settings-glass-opacity {
4578
+ --liquid-range-progress: 85%;
4579
+ height: 6px;
4580
+ border-radius: var(--radius-full);
4581
+ background: linear-gradient(90deg, var(--accent) 0 var(--liquid-range-progress), var(--glass-border-2) var(--liquid-range-progress) 100%);
4582
+ cursor: pointer;
4583
+ }
4584
+ #settings-glass-opacity::-webkit-slider-thumb {
4585
+ width: 16px;
4586
+ height: 16px;
4587
+ border: 0;
4588
+ background: var(--accent);
4589
+ box-shadow: 0 2px 6px rgba(0,0,0,.3);
4590
+ 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);
4591
+ }
4592
+ #settings-glass-opacity.liquid-range-dragging::-webkit-slider-thumb {
4593
+ width: 26px;
4594
+ height: 20px;
4595
+ border-radius: var(--radius-full);
4596
+ background: var(--liquid-refraction);
4597
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
4598
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
4599
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 8px 20px rgba(0,0,0,.34);
4600
+ transform: translateY(-2px) scale(1.22);
4601
+ transition: none;
4602
+ }
4603
+ .settings-glass-range-line { display:flex; align-items:center; gap:10px; }
4604
+ .settings-glass-range-line #settings-glass-opacity { flex:1; min-width:0; }
4605
+ #settings-glass-value {
4606
+ min-width: 42px;
4607
+ color: var(--text-bright);
4608
+ font: 650 11px var(--font-mono);
4609
+ text-align: right;
4610
+ font-variant-numeric: tabular-nums;
4611
+ }
4612
+
4394
4613
  .setting-desc {
4395
4614
  font-size: 10px;
4396
4615
  color: var(--text-dim);
@@ -4504,9 +4723,13 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4504
4723
  }
4505
4724
 
4506
4725
  [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);
4726
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 2.2)) saturate(165%) brightness(1.01);
4727
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 2.2)) saturate(165%) brightness(1.01);
4728
+ border-color: rgba(54,68,106,0.14);
4729
+ box-shadow: var(--modal-shadow);
4730
+ }
4731
+ [data-theme="light"] .sub-win.liquid-glass-carrier {
4732
+ background: var(--settings-modal-surface) !important;
4510
4733
  }
4511
4734
  .memory-lab-search-results[hidden] {
4512
4735
  display: none;
@@ -4521,7 +4744,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4521
4744
  align-items: center;
4522
4745
  gap: 4px;
4523
4746
  padding: 3px;
4524
- border-radius: var(--radius-md);
4747
+ border-radius: var(--radius-full);
4525
4748
  border: 1px solid var(--glass-border-1);
4526
4749
  background: var(--glass-bg-1);
4527
4750
  }
@@ -4572,7 +4795,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4572
4795
  .memory-lab-overview-actions select {
4573
4796
  height: 26px;
4574
4797
  padding: 0 9px;
4575
- border-radius: var(--radius-sm);
4798
+ border-radius: var(--radius-full);
4576
4799
  border: 1px solid transparent;
4577
4800
  background: transparent;
4578
4801
  color: var(--text-dim);
@@ -5400,6 +5623,202 @@ html.newmark-memory-overview-viewer body {
5400
5623
  opacity: 0.8;
5401
5624
  }
5402
5625
 
5626
+
5627
+ /* ============================================================
5628
+ LIQUID GLASS (Kyant AndroidLiquidGlass backdrop parity)
5629
+ CSS equivalent of the library lens/edge refraction, top
5630
+ highlight, inner shadow, outer shadow and vibrancy boost.
5631
+ ============================================================ */
5632
+ .liquid-glass {
5633
+ position: relative;
5634
+ backdrop-filter: blur(var(--glass-blur-3)) saturate(140%) brightness(1.02);
5635
+ -webkit-backdrop-filter: blur(var(--glass-blur-3)) saturate(140%) brightness(1.02);
5636
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), var(--liquid-outer-shadow);
5637
+ }
5638
+ .liquid-glass::before {
5639
+ content: '';
5640
+ position: absolute;
5641
+ inset: 0;
5642
+ pointer-events: none;
5643
+ border-radius: inherit;
5644
+ background: var(--liquid-refraction);
5645
+ opacity: 0.9;
5646
+ mix-blend-mode: screen;
5647
+ }
5648
+ .liquid-glass::after {
5649
+ content: '';
5650
+ position: absolute;
5651
+ inset: 0;
5652
+ pointer-events: none;
5653
+ border-radius: inherit;
5654
+ border: 1px solid var(--liquid-edge-glow);
5655
+ box-shadow: inset 0 0 0 0.5px var(--liquid-edge-glow-strong);
5656
+ }
5657
+ .liquid-glass-strong {
5658
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 1.5)) saturate(150%) brightness(1.04);
5659
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 1.5)) saturate(150%) brightness(1.04);
5660
+ }
5661
+ [data-theme='light'] .liquid-glass {
5662
+ backdrop-filter: none;
5663
+ -webkit-backdrop-filter: none;
5664
+ }
5665
+ [data-theme='light'] .liquid-glass::before {
5666
+ opacity: 0.45;
5667
+ mix-blend-mode: normal;
5668
+ }
5669
+ .liquid-glass-carrier {
5670
+ background: rgb(var(--glass-rgb-2) / var(--carrier-glass-alpha)) !important;
5671
+ backdrop-filter: blur(var(--carrier-glass-blur)) saturate(150%) brightness(1.02) !important;
5672
+ -webkit-backdrop-filter: blur(var(--carrier-glass-blur)) saturate(150%) brightness(1.02) !important;
5673
+ }
5674
+ /* List popups use the live carrier refraction only. The old static outer
5675
+ glow/shadow painted a second fixed background behind the popup. */
5676
+ .conv-action-menu.liquid-glass,
5677
+ .model-select-menu.liquid-glass,
5678
+ .newmark-select-menu.liquid-glass {
5679
+ box-shadow: none;
5680
+ }
5681
+ .conv-action-menu.liquid-glass::before,
5682
+ .model-select-menu.liquid-glass::before,
5683
+ .newmark-select-menu.liquid-glass::before {
5684
+ background: none;
5685
+ opacity: 0;
5686
+ mix-blend-mode: normal;
5687
+ }
5688
+ .conv-action-menu.liquid-glass::after,
5689
+ .model-select-menu.liquid-glass::after,
5690
+ .newmark-select-menu.liquid-glass::after {
5691
+ box-shadow: none;
5692
+ }
5693
+ .liquid-selection-canvas { opacity: var(--liquid-float-optical-opacity); }
5694
+
5695
+ /* Buttons retain their component-owned static surface. The Kyant-like glass
5696
+ layer exists only during press and falls away with the color block. */
5697
+ button {
5698
+ position: relative;
5699
+ isolation: isolate;
5700
+ transition-property: scale, filter, border-color, background-color, box-shadow, color;
5701
+ transition-duration: 120ms;
5702
+ transition-timing-function: var(--ease-out-expo);
5703
+ }
5704
+ [data-theme="light"] .conv-action-menu,
5705
+ [data-theme="light"] .model-select-menu,
5706
+ [data-theme="light"] .newmark-select-menu,
5707
+ [data-theme="light"] .sub-win {
5708
+ background-color: transparent;
5709
+ }
5710
+ button::after {
5711
+ content: '';
5712
+ position: absolute;
5713
+ inset: -2px;
5714
+ z-index: -1;
5715
+ pointer-events: none;
5716
+ border-radius: inherit;
5717
+ opacity: 0;
5718
+ background: var(--liquid-refraction);
5719
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
5720
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
5721
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 4px 14px rgba(0,0,0,0.18);
5722
+ transition: opacity 90ms var(--ease-out-expo);
5723
+ }
5724
+ button:hover:not(:disabled) {
5725
+ filter: brightness(1.06);
5726
+ }
5727
+ button:active:not(:disabled) {
5728
+ scale: 1.03;
5729
+ filter: brightness(1.04);
5730
+ background-color: transparent !important;
5731
+ box-shadow: none;
5732
+ }
5733
+ button:active:not(:disabled)::after { opacity: 1; }
5734
+ button:disabled { opacity: 0.5; }
5735
+
5736
+ /* Borderless utility actions stay visually quiet; they never gain a glass plate. */
5737
+ .msg-action-btn::after,
5738
+ .top-btn::after,
5739
+ .conv-more-btn::after,
5740
+ .provider-icon-btn::after,
5741
+ .work-review-toggle::after,
5742
+ button.conversation-work-run-head::after,
5743
+ button.shell-block-header::after,
5744
+ button.diff-block-header::after,
5745
+ button.work-review-head::after { display: none !important; }
5746
+ button.conversation-work-run-head:active:not(:disabled),
5747
+ button.shell-block-header:active:not(:disabled),
5748
+ button.diff-block-header:active:not(:disabled),
5749
+ button.work-review-head:active:not(:disabled) {
5750
+ scale: 1;
5751
+ filter: none;
5752
+ background-color: revert !important;
5753
+ }
5754
+
5755
+ /* Two-position liquid track. The thumb becomes glass only while held. */
5756
+ input[type="checkbox"] {
5757
+ --liquid-switch-x: 0px;
5758
+ --liquid-switch-progress: 0%;
5759
+ appearance: none;
5760
+ -webkit-appearance: none;
5761
+ width: 42px !important;
5762
+ height: 24px !important;
5763
+ min-width: 42px;
5764
+ margin: 0;
5765
+ padding: 0;
5766
+ border: 1px solid var(--glass-border-2);
5767
+ border-radius: var(--radius-full);
5768
+ background: linear-gradient(90deg, var(--accent) 0 var(--liquid-switch-progress), var(--glass-bg-3) var(--liquid-switch-progress) 100%);
5769
+ cursor: pointer;
5770
+ position: relative;
5771
+ vertical-align: middle;
5772
+ transition: background 160ms var(--ease-out-expo), border-color 160ms var(--ease-out-expo);
5773
+ }
5774
+ input[type="checkbox"]:checked {
5775
+ --liquid-switch-x: 18px;
5776
+ --liquid-switch-progress: 100%;
5777
+ border-color: color-mix(in srgb, var(--accent) 72%, white);
5778
+ }
5779
+ input[type="checkbox"]::before {
5780
+ content: '';
5781
+ position: absolute;
5782
+ left: 2px;
5783
+ top: 2px;
5784
+ width: 18px;
5785
+ height: 18px;
5786
+ border-radius: 50%;
5787
+ background: var(--text-bright);
5788
+ box-shadow: 0 2px 5px rgba(0,0,0,.28);
5789
+ transform: translateX(var(--liquid-switch-x)) scale(1);
5790
+ transition: transform 160ms var(--ease-out-expo), box-shadow 100ms var(--ease-out-expo), background 100ms var(--ease-out-expo);
5791
+ }
5792
+ input[type="checkbox"].liquid-switch-dragging::before {
5793
+ left: 1px;
5794
+ width: 22px;
5795
+ border-radius: var(--radius-full);
5796
+ background: var(--liquid-refraction);
5797
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
5798
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%);
5799
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 7px 18px rgba(0,0,0,.32);
5800
+ transform: translateX(var(--liquid-switch-x)) translateY(-2px) scale(1.22);
5801
+ transition: none;
5802
+ }
5803
+
5804
+ /* Center composer and terminal stay solid. Only their left/right edge catches
5805
+ light so the glass sidebars remain the visual frame. */
5806
+ #input-area,
5807
+ #bottom {
5808
+ backdrop-filter: none;
5809
+ -webkit-backdrop-filter: none;
5810
+ box-shadow: inset 1px 0 0 var(--liquid-edge-glow), inset -1px 0 0 var(--liquid-edge-glow);
5811
+ }
5812
+
5813
+ /* The live indicator is strictly a continuous black/white/black/white border.
5814
+ Keep it above generic glass pseudo-elements when classes are combined. */
5815
+ .marquee-border::before,
5816
+ .liquid-glass.marquee-border::before {
5817
+ background: conic-gradient(from var(--marquee-angle, 0deg), #000 0deg, #fff 90deg, #000 180deg, #fff 270deg, #000 360deg);
5818
+ opacity: 1;
5819
+ mix-blend-mode: normal;
5820
+ animation: marquee-rotate var(--marquee-speed) linear infinite;
5821
+ }
5403
5822
  </style>
5404
5823
  </head>
5405
5824
  <body>
@@ -5625,6 +6044,10 @@ html.newmark-memory-overview-viewer body {
5625
6044
  <symbol id="minus" viewBox="0 0 24 24">
5626
6045
  <path d="M5 12h14" />
5627
6046
  </symbol>
6047
+ <symbol id="mouse" viewBox="0 0 24 24">
6048
+ <rect x="5" y="2" width="14" height="20" rx="7" />
6049
+ <path d="M12 6v4" />
6050
+ </symbol>
5628
6051
  <symbol id="move" viewBox="0 0 24 24">
5629
6052
  <path d="M12 2v20" />
5630
6053
  <path d="m15 19-3 3-3-3" />
@@ -5875,6 +6298,7 @@ html.newmark-memory-overview-viewer body {
5875
6298
  <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
6299
  </div>
5877
6300
  <div id="left-content">
6301
+ <div id="left-tool-surface">
5878
6302
  <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
6303
  <button type="button" class="left-nav-icon" onclick="window.showNewConversationPage()" title="New conversation">
5880
6304
  <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 +6318,7 @@ html.newmark-memory-overview-viewer body {
5894
6318
  <button type="button" class="left-nav-icon" onclick="window.openSettings()">
5895
6319
  <span class="icon"><svg class="nm-icon small" aria-hidden="true" focusable="false"><use href="#settings"></use></svg></span><span>Settings</span>
5896
6320
  </button>
6321
+ </div>
5897
6322
  <div id="left-ws-section">
5898
6323
  <div id="left-ws-header">Workspaces</div>
5899
6324
  <div id="left-ws-list" role="list" aria-label="Workspaces"></div>
@@ -5993,9 +6418,9 @@ html.newmark-memory-overview-viewer body {
5993
6418
  <div class="model-select-shell" id="model-select-shell">
5994
6419
  <select class="model-select-native" id="model-select" tabindex="-1" aria-hidden="true"></select>
5995
6420
  <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>
6421
+ <div class="model-select-menu liquid-glass liquid-glass-carrier" id="model-select-menu" role="listbox" aria-label="Model" popover="manual"></div>
5997
6422
  </div>
5998
- <div class="conv-action-menu" id="conversation-action-menu" role="menu" aria-label="Conversation actions" popover="manual"></div>
6423
+ <div class="conv-action-menu liquid-glass liquid-glass-carrier" id="conversation-action-menu" role="menu" aria-label="Conversation actions" popover="manual"></div>
5999
6424
  <div class="context-token-wrap">
6000
6425
  <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
6426
  <div id="context-token-tooltip"></div>
@@ -6113,7 +6538,7 @@ html.newmark-memory-overview-viewer body {
6113
6538
 
6114
6539
  <!-- ========== SUB-WINDOW OVERLAY ========== -->
6115
6540
  <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">
6541
+ <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
6542
  <div class="sub-win-header" id="sub-win-header">
6118
6543
  <span class="sub-win-title" id="sub-win-title">Window</span>
6119
6544
  <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 +6717,7 @@ var state = {
6292
6717
  activeSendCallsByTarget: {},
6293
6718
  backendQueue: { steering: [], followUp: [] },
6294
6719
  backendQueuesByTarget: {},
6720
+ queueItemsByTarget: {},
6295
6721
  defaultFlow: '',
6296
6722
  settingsActiveTab: 'general',
6297
6723
  globalAgentPrompt: '',
@@ -7077,6 +7503,7 @@ var NEWMARK_I18N = {
7077
7503
  'conversation.branchCommunicationBadge': 'Branch communication',
7078
7504
  'conversation.loadingIsolated': 'Loading isolated conversation...',
7079
7505
  'conversation.locked': 'Current conversation is locked while the agent is working.',
7506
+ 'conversation.emptyWorkspace': 'No conversations in this workspace yet.',
7080
7507
  'queue.next': 'Next',
7081
7508
  'terminal.notConnected': 'Terminal not connected',
7082
7509
  'common.error': 'Error',
@@ -7808,6 +8235,7 @@ var NEWMARK_I18N = {
7808
8235
  'conversation.branchCommunicationBadge': '分支交流',
7809
8236
  'conversation.loadingIsolated': '正在加载隔离对话...',
7810
8237
  'conversation.locked': 'Agent 工作中,当前对话已锁定。',
8238
+ 'conversation.emptyWorkspace': '当前工作区还没有对话。',
7811
8239
  'queue.next': '下一轮',
7812
8240
  'terminal.notConnected': '终端未连接',
7813
8241
  'common.error': '错误',
@@ -7920,9 +8348,9 @@ function applyLanguageToUi() {
7920
8348
  var nextBtn = document.querySelector('.mode-toggle-btn[data-mode="next"]');
7921
8349
  if (guideBtn) guideBtn.textContent = t('input.guide');
7922
8350
  if (nextBtn) nextBtn.textContent = t('input.next');
7923
- var toolsHead = document.querySelector('#left-content > div:first-child');
8351
+ var toolsHead = document.querySelector('#left-tool-surface > div:first-child');
7924
8352
  if (toolsHead) toolsHead.textContent = t('left.tools');
7925
- var navs = document.querySelectorAll('#left-content .left-nav-icon');
8353
+ var navs = document.querySelectorAll('#left-tool-surface .left-nav-icon');
7926
8354
  var navLabels = [t('left.newChat'), t('left.plugins'), t('left.memoryLab'), t('left.automation'), t('left.flow'), t('left.settings')];
7927
8355
  for (var i = 0; i < navs.length && i < navLabels.length; i++) {
7928
8356
  var span = navs[i].querySelector('span:last-child');
@@ -8395,6 +8823,30 @@ function syncTerminalShellOptions() {
8395
8823
 
8396
8824
  // === API Bridge ===
8397
8825
  var api = window.api || {
8826
+ queueAction: async function(action, input) {
8827
+ try {
8828
+ var target = normalizeQueuedConversationTarget(currentConversationTarget());
8829
+ if (!target) return { ok: false, error: 'No active conversation target' };
8830
+ var body = {
8831
+ workspaceId: target.workspaceId,
8832
+ conversationId: target.conversationId,
8833
+ action: action,
8834
+ id: String(input && input.id || ''),
8835
+ text: String(input && input.text || ''),
8836
+ value: String(input && input.text || ''),
8837
+ requestedMode: String(input && input.requestedMode || 'build'),
8838
+ goalObjective: String(input && input.goalObjective || ''),
8839
+ createdAt: String(input && input.createdAt || ''),
8840
+ };
8841
+ if (Array.isArray(input && input.orderedIds)) body.orderedIds = input.orderedIds;
8842
+ var r = await fetch('/api/queue-action', {
8843
+ method: 'POST',
8844
+ headers: { 'Content-Type': 'application/json' },
8845
+ body: JSON.stringify(body)
8846
+ });
8847
+ return r.json();
8848
+ } catch(e) { return { ok: false, error: e.message }; }
8849
+ },
8398
8850
  sendMessage: async function(msg, conversationId) {
8399
8851
  try {
8400
8852
  var body = {message: msg, mode: state.mode, model: state.model, intelligence: state.intelligence, inputMode: state.inputMode};
@@ -9316,24 +9768,84 @@ window.openWorkReviewFile = async function(path) {
9316
9768
  return true;
9317
9769
  };
9318
9770
 
9319
- function addWorkReview(diffs) {
9771
+ function workReviewRunIds(runId, aliases) {
9772
+ var ids = [];
9773
+ [runId].concat(Array.isArray(aliases) ? aliases : []).forEach(function(value) {
9774
+ var id = String(value || '');
9775
+ if (id && ids.indexOf(id) < 0) ids.push(id);
9776
+ });
9777
+ return ids;
9778
+ }
9779
+
9780
+ function upsertWorkReviewRecord(ui, diffs, runId, aliases) {
9781
+ if (!ui) return null;
9782
+ if (!Array.isArray(ui.workReviews)) ui.workReviews = [];
9783
+ var ids = workReviewRunIds(runId, aliases);
9784
+ var canonicalRunId = String(runId || ids[0] || '');
9785
+ var record = { diffs: diffs, runId: canonicalRunId };
9786
+ var matchIndex = -1;
9787
+ for (var index = 0; index < ui.workReviews.length; index++) {
9788
+ var itemRunId = String(ui.workReviews[index] && ui.workReviews[index].runId || '');
9789
+ if (ids.indexOf(itemRunId) < 0) continue;
9790
+ if (matchIndex < 0) {
9791
+ matchIndex = index;
9792
+ ui.workReviews[index] = record;
9793
+ } else {
9794
+ ui.workReviews.splice(index, 1);
9795
+ index--;
9796
+ }
9797
+ }
9798
+ if (matchIndex < 0) ui.workReviews.push(record);
9799
+ return record;
9800
+ }
9801
+
9802
+ function addWorkReview(diffs, runId, aliases) {
9320
9803
  var files = normalizeWorkReviewDiffs(diffs);
9321
9804
  if (!files.length) return null;
9805
+ var filesKey = JSON.stringify(files);
9806
+ var reviewRunId = String(runId || '');
9807
+ var reviewRunIds = workReviewRunIds(reviewRunId, aliases);
9808
+ var chatArea = els['chat-area'];
9809
+ var review = null;
9810
+ if (chatArea) {
9811
+ var existingReviews = chatArea.querySelectorAll('.work-review');
9812
+ for (var ri = 0; ri < existingReviews.length; ri++) {
9813
+ var sameFiles = existingReviews[ri].getAttribute('data-files') === filesKey;
9814
+ var existingRunId = String(existingReviews[ri].getAttribute('data-run-id') || '');
9815
+ var sameRun = reviewRunIds.length ? reviewRunIds.indexOf(existingRunId) >= 0 : sameFiles && !existingRunId;
9816
+ if (!sameRun) continue;
9817
+ if (!review) review = existingReviews[ri];
9818
+ else existingReviews[ri].remove();
9819
+ }
9820
+ }
9322
9821
  var added = files.reduce(function(total, file) { return total + file.added; }, 0);
9323
9822
  var deleted = files.reduce(function(total, file) { return total + file.deleted; }, 0);
9324
- var review = document.createElement('div');
9325
- review.className = 'work-review collapsed';
9823
+ var wasCollapsed = review ? review.classList.contains('collapsed') : true;
9824
+ var wasExpanded = review ? review.classList.contains('expanded') : false;
9825
+ if (!review) review = document.createElement('div');
9826
+ review.className = 'work-review' + (wasCollapsed ? ' collapsed' : '') + (wasExpanded ? ' expanded' : '');
9326
9827
  review.setAttribute('data-files', JSON.stringify(files));
9828
+ if (reviewRunId) review.setAttribute('data-run-id', reviewRunId);
9829
+ else review.removeAttribute('data-run-id');
9327
9830
  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>';
9831
+ 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
9832
  }).join('');
9330
9833
  var editedLabel = files.length === 1 ? t('review.editedOne') : t('review.editedMany').replace('{count}', files.length);
9331
9834
  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
9835
  '<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
9836
  '<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);
9837
+ '<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>' +
9838
+ '<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>';
9839
+ var owningRun = null;
9840
+ var renderedRuns = reviewRunId ? els['chat-area'].querySelectorAll('.conversation-work-run[data-run-id]') : [];
9841
+ for (var wi = 0; wi < renderedRuns.length; wi++) {
9842
+ if (renderedRuns[wi].getAttribute('data-run-id') === reviewRunId) { owningRun = renderedRuns[wi]; break; }
9843
+ }
9844
+ var owningMessage = owningRun && owningRun.closest ? owningRun.closest('.work-run-message') : null;
9845
+ if (!review.parentNode) {
9846
+ if (owningMessage && owningMessage.parentNode === els['chat-area']) owningMessage.insertAdjacentElement('afterend', review);
9847
+ else els['chat-area'].appendChild(review);
9848
+ }
9337
9849
  autoScrollIfAtBottom();
9338
9850
  return review;
9339
9851
  }
@@ -9701,12 +10213,25 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
9701
10213
  persistedGuideIds = persistedGuideIds || {};
9702
10214
  var messages = guideMessagesForTarget(target);
9703
10215
  var pending = Object.keys(messages).map(function(id) { return messages[id]; }).filter(function(item) {
9704
- return item && !persistedGuideIds[item.clientMessageId];
10216
+ if (!item || persistedGuideIds[item.clientMessageId]) return false;
10217
+ // A Guide already owned by its work run is rendered by the Build block
10218
+ // (expanded events or collapsed guide list), never as a separate trailing
10219
+ // message. Without this guard, collapse/expand rebuilds would re-add the
10220
+ // applied Guide at the end of the chat.
10221
+ if (workRunOwnsGuide(item.runId, item.clientMessageId, target)) return false;
10222
+ return true;
9705
10223
  }).sort(function(a, b) {
9706
10224
  return String(a.createdAt || '').localeCompare(String(b.createdAt || ''));
9707
10225
  });
9708
10226
  pending.forEach(function(item) {
9709
10227
  var element = findGuideMessageElement(item.clientMessageId);
10228
+ if (element && !item.awaitingAck && item.runId && workRunOwnsGuide(item.runId, item.clientMessageId, target)) {
10229
+ // The Guide is owned by its work run but a stale standalone copy exists
10230
+ // (e.g. created before this guard). Remove it so the Build block stays
10231
+ // the single source of truth.
10232
+ element.remove();
10233
+ element = null;
10234
+ }
9710
10235
  if (item.awaitingAck && item.runId) {
9711
10236
  var runElement = findWorkRunElement({ runId: item.runId });
9712
10237
  var runBody = runElement && runElement.querySelector('.conversation-work-run-body');
@@ -9738,6 +10263,22 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
9738
10263
  return pending.length;
9739
10264
  }
9740
10265
 
10266
+ function workRunOwnsGuide(runId, clientMessageId, target) {
10267
+ if (!runId || !clientMessageId) return false;
10268
+ var runs = workRunsForTarget(target || currentConversationTarget());
10269
+ var run = runs.find(function(item) { return String(item.runId || '') === String(runId); });
10270
+ if (!run) return false;
10271
+ function matches(guide) {
10272
+ return guide && String(guide.clientMessageId || '') === String(clientMessageId);
10273
+ }
10274
+ if ((run.guides || []).some(matches)) return true;
10275
+ return (run.events || []).some(function(event) {
10276
+ if (!event) return false;
10277
+ if (matches(event.guide)) return true;
10278
+ return !!(event.clientMessageId && String(event.clientMessageId) === String(clientMessageId));
10279
+ });
10280
+ }
10281
+
9741
10282
  function eventRuntimeKey(event) {
9742
10283
  var target = {
9743
10284
  workspaceId: event && (event.workspaceId || (event.target && event.target.workspaceId)),
@@ -10508,23 +11049,29 @@ function workRunGuideEvents(run) {
10508
11049
  });
10509
11050
  }
10510
11051
 
10511
- function renderWorkRunEvents(run, includeGuides) {
11052
+ function presentedWorkRunEvents(run, includeGuides) {
10512
11053
  var rawEvents = (run.events || []).filter(publicWorkEvent).filter(function(event) {
10513
11054
  return includeGuides !== false || !(String(event && event.type || '').toLowerCase().indexOf('guide') === 0 || !!(event && event.guide));
10514
11055
  }).sort(compareConversationWorkEvents);
10515
11056
  var terminalInterrupted = ['interrupted', 'force_interrupted'].indexOf(String(run && run.status || '')) >= 0;
10516
11057
  var events = [];
10517
11058
  var publicText = '';
11059
+ // 公共文本段继承其最后一条 text 事件的 sequence:增量渲染依赖 presented
11060
+ // 顺序的单调性,无 sequence 的文本段会被排到列表前部,导致每次文本增量
11061
+ // 都从前部重建后续节点。
11062
+ var publicTextSequence = 0;
10518
11063
  function flushPublicText() {
10519
11064
  var content = publicText.trim().slice(0, 50000);
10520
11065
  publicText = '';
10521
- if (content) events.push({ type: terminalInterrupted ? 'partial_text' : 'public_text', content: content });
11066
+ if (content) events.push({ type: terminalInterrupted ? 'partial_text' : 'public_text', content: content, sequence: publicTextSequence });
11067
+ publicTextSequence = 0;
10522
11068
  }
10523
11069
  for (var eventIndex = 0; eventIndex < rawEvents.length; eventIndex++) {
10524
11070
  var rawEvent = rawEvents[eventIndex];
10525
11071
  var rawType = String(rawEvent && rawEvent.type || '').toLowerCase();
10526
11072
  if (rawType === 'text') {
10527
11073
  publicText += String(rawEvent.content || '');
11074
+ publicTextSequence = Math.max(publicTextSequence, Number(rawEvent.sequence || 0));
10528
11075
  continue;
10529
11076
  }
10530
11077
  if (rawType === 'response') {
@@ -10596,46 +11143,120 @@ function renderWorkRunEvents(run, includeGuides) {
10596
11143
  }
10597
11144
  presentedEvents.push({ type: 'tool_group', count: count, completed: allCompleted, toolName: presented.toolName, items: activityItems });
10598
11145
  }
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>';
11146
+ return { events: presentedEvents, terminalInterrupted: terminalInterrupted };
11147
+ }
11148
+
11149
+ // 每个呈现节点的稳定 key:内容变化(增长/完成)时 key 变化,增量渲染据此
11150
+ // 只重建变化节点之后的 DOM,前缀节点原样保留。
11151
+ function workRunPresentedEventKey(event, eventIndex) {
11152
+ var type = String(event && event.type || 'status').toLowerCase();
11153
+ if (type === 'public_text' || type === 'partial_text' || type === 'response') {
11154
+ return type + ':' + String(event && event.content || '').length;
11155
+ }
11156
+ if (type === 'thought') {
11157
+ return 'thought:' + String(event && (event.id || event.toolCallId) || '') + '@' + String(event && event.content || '').length + ':' + (event && event.completed ? '1' : '0');
11158
+ }
11159
+ if (type === 'tool_group') {
11160
+ return 'tool_group:' + ((event && event.items || []).map(function(item) {
11161
+ return String(item && (item.id || item.toolCallId) || '') + ':' + (item && item.completed ? '1' : '0');
11162
+ }).join(',')) + '#n' + Number(event && event.count || 0);
11163
+ }
11164
+ if (type.indexOf('guide') === 0 || (event && event.guide)) {
11165
+ return 'guide:' + guideWorkEventKey(event) + ':' + guideWorkEventStatus(event);
11166
+ }
11167
+ var activityTail = type.indexOf('tool_') === 0 && type !== 'tool_call' && type !== 'tool_result'
11168
+ ? ':' + String(event && event.activity && event.activity.key || '') + ':' + Number(event && event.count || 1)
11169
+ : '';
11170
+ return type + ':' + String(event && (event.id || [event && event.sequence || '', event && event.toolName || '', event && event.timestamp || ''].join('|')) || '') + ':' + (event && event.completed ? '1' : '0') + activityTail;
11171
+ }
11172
+
11173
+ function renderWorkRunEventNode(event, eventIndex, run, terminalInterrupted) {
11174
+ var type = String(event.type || 'status').toLowerCase();
11175
+ if (type === 'public_text' || type === 'partial_text' || type === 'response') {
11176
+ var responseLabel = type === 'response' && terminalInterrupted
11177
+ ? (currentLang() === 'zh' ? '未完成回复片段\n' : 'Incomplete response fragment\n') + workEventLabel(event)
11178
+ : workEventLabel(event);
11179
+ // While a run is still receiving text, this view can be rebuilt once per
11180
+ // event. Avoid invoking the full Markdown parser for a large live
11181
+ // narrative; the final response path still gets the normal rich render.
11182
+ var liveNarrative = ['running', 'stopping', 'force_restarting'].indexOf(String(run && run.status || '').toLowerCase()) >= 0;
11183
+ var narrativeHtml = liveNarrative && String(responseLabel || '').length >= STREAMING_PLAIN_TEXT_THRESHOLD
11184
+ ? esc(responseLabel)
11185
+ : renderMessageContent(responseLabel);
11186
+ return '<div class="conversation-work-event narrative"><div class="conversation-work-event-content">' + narrativeHtml + '</div></div>';
11187
+ }
11188
+ if (type.indexOf('guide') === 0 || event.guide) return renderWorkRunGuideMessage(event);
11189
+ if (type === 'tool_group') return renderWorkToolGroup(event, eventIndex);
11190
+ if (type === 'thought') return renderWorkThought(event, eventIndex);
11191
+ var activitySummary = type.indexOf('tool_') === 0 && type !== 'tool_call' && type !== 'tool_result';
11192
+ var cls = type.indexOf('guide') === 0 ? ' guide' : (type === 'error' ? ' error' : (activitySummary ? ' activity-summary' : ''));
11193
+ var label = activitySummary ? workToolActivityLabel(event.activity, Number(event.count || 1), !!event.completed) : workEventLabel(event) + (event.completed ? (currentLang() === 'zh' ? ' · 已完成' : ' · completed') : '');
11194
+ if (activitySummary) {
11195
+ var firstActivityItem = event.items && event.items[0] || {};
11196
+ var activityDomKey = String(firstActivityItem.id || firstActivityItem.toolCallId || ((event.activity && event.activity.key || type) + '-' + eventIndex));
11197
+ var detailRows = (event.items || []).map(function(item) {
11198
+ var detail = String(item.toolArgs || '').trim();
11199
+ return '<div class="conversation-work-activity-item">' + iconSvg(workEventIcon('tool_call'), 'tool_call', 'tiny') +
11200
+ '<div><div>' + esc(publicToolNameForUi(item.toolName)) + (item.completed ? (currentLang() === 'zh' ? ' · 已完成' : ' · completed') : '') + '</div>' +
11201
+ (detail ? '<div class="conversation-work-activity-detail">' + esc(detail) + '</div>' : '') + '</div></div>';
11202
+ }).join('');
11203
+ 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>' +
11204
+ '<div class="conversation-work-activity-list">' + detailRows + '</div></details>';
11205
+ }
11206
+ return '<div class="conversation-work-event' + cls + '">' + iconSvg(workEventIcon(type), type, 'tiny') +
11207
+ '<div class="conversation-work-event-content">' + renderMessageContent(label) + '</div></div>';
11208
+ }
11209
+
11210
+ // 给单个事件节点的 HTML 根标签插入稳定 key,使首次渲染的节点与增量
11211
+ // diff 使用同一 key 体系(否则第一次增量更新会全量失配重建)。
11212
+ function withWorkEventKey(html, key) {
11213
+ var match = /^<([A-Za-z][\w-]*)([^>]*)>/.exec(String(html || ''));
11214
+ if (!match) return html;
11215
+ return '<' + match[1] + match[2] + ' data-work-event-key="' + escAttr(String(key || '')) + '">' + html.slice(match[0].length);
11216
+ }
11217
+
11218
+ function renderWorkRunEvents(run, includeGuides) {
11219
+ var presented = presentedWorkRunEvents(run, includeGuides);
11220
+ if (!presented.events.length) return '<div class="conversation-work-empty">' + esc(currentLang() === 'zh' ? '等待公开工作进度…' : 'Waiting for public progress…') + '</div>';
11221
+ return '<div class="conversation-work-run-events">' + presented.events.map(function(event, eventIndex) {
11222
+ return withWorkEventKey(renderWorkRunEventNode(event, eventIndex, run, presented.terminalInterrupted), workRunPresentedEventKey(event, eventIndex));
10636
11223
  }).join('') + '</div>';
10637
11224
  }
10638
11225
 
11226
+ // 增量更新 run body 内的事件列表:保留与新呈现列表 key 一致的前缀节点,
11227
+ // 从第一个分歧节点起重建尾部。运行中通常只有最后一个文本/thought 节点
11228
+ // 增长或尾部追加,因此不会整块重绘整个进程渲染。
11229
+ function updateWorkRunEventsContainer(run, body, expanded) {
11230
+ var presented = presentedWorkRunEvents(run, expanded);
11231
+ var container = body.querySelector ? body.querySelector('.conversation-work-run-events') : null;
11232
+ if (!container) {
11233
+ body.innerHTML = renderWorkRunEvents(run, expanded);
11234
+ return;
11235
+ }
11236
+ if (!presented.events.length) {
11237
+ var emptyHtml = '<div class="conversation-work-empty">' + esc(currentLang() === 'zh' ? '等待公开工作进度…' : 'Waiting for public progress…') + '</div>';
11238
+ if (container.innerHTML !== emptyHtml) container.innerHTML = emptyHtml;
11239
+ return;
11240
+ }
11241
+ var nodes = Array.prototype.slice.call(container.children);
11242
+ var keep = 0;
11243
+ while (keep < nodes.length && keep < presented.events.length) {
11244
+ if (String(nodes[keep].getAttribute('data-work-event-key') || '') === workRunPresentedEventKey(presented.events[keep], keep)) keep++;
11245
+ else break;
11246
+ }
11247
+ while (container.children.length > keep) container.removeChild(container.lastChild);
11248
+ for (var i = keep; i < presented.events.length; i++) {
11249
+ var key = workRunPresentedEventKey(presented.events[i], i);
11250
+ var holder = document.createElement('div');
11251
+ holder.innerHTML = renderWorkRunEventNode(presented.events[i], i, run, presented.terminalInterrupted);
11252
+ while (holder.firstChild) {
11253
+ var node = holder.firstChild;
11254
+ node.setAttribute('data-work-event-key', key);
11255
+ container.appendChild(node);
11256
+ }
11257
+ }
11258
+ }
11259
+
10639
11260
  function findWorkRunElement(run) {
10640
11261
  if (!els['chat-area']) return null;
10641
11262
  var nodes = els['chat-area'].querySelectorAll('.conversation-work-run');
@@ -10648,6 +11269,11 @@ function findWorkRunElement(run) {
10648
11269
  function updateConversationWorkRunElement(run, element) {
10649
11270
  element = element || findWorkRunElement(run);
10650
11271
  if (!element) return null;
11272
+ // 重建/增量更新前后保持聊天视窗滚动位置:贴底时跟随最新,未贴底时
11273
+ // 精确恢复 scrollTop,避免内容高度变化把视窗“强制拖向最新”。
11274
+ var chatEl = els['chat-area'];
11275
+ var wasAtBottom = !!(chatEl && shouldAutoScroll(chatEl));
11276
+ var scrollTopBefore = chatEl ? chatEl.scrollTop : 0;
10651
11277
  var activityOpenStates = {};
10652
11278
  var priorActivities = element.querySelectorAll ? element.querySelectorAll('details.conversation-work-activity[data-activity-key], details[data-work-detail-key]') : [];
10653
11279
  for (var activityIndex = 0; activityIndex < priorActivities.length; activityIndex++) {
@@ -10662,12 +11288,51 @@ function updateConversationWorkRunElement(run, element) {
10662
11288
  element.className = 'conversation-work-run ' + (running ? 'running ' : '') + (expanded ? 'expanded' : 'collapsed');
10663
11289
  element.setAttribute('data-run-id', run.runId);
10664
11290
  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>';
11291
+ var head = element.querySelector ? element.querySelector('.conversation-work-run-head') : null;
11292
+ if (!head) {
11293
+ element.innerHTML = '<button class="conversation-work-run-head" type="button" onclick="window.toggleConversationWorkRun(this)">' +
11294
+ '<span class="conversation-work-run-title">' + esc(workRunTitle(run)) + '</span>' +
11295
+ workRunChangeBadge(run) +
11296
+ '<span class="conversation-work-run-status">' + esc(String(run.status || '')) + '</span>' +
11297
+ '<span class="conversation-work-run-chevron" aria-hidden="true"></span></button>' +
11298
+ '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>';
11299
+ } else {
11300
+ // 就地更新头部,不重建 head 按钮(保住聚焦/动画状态)。
11301
+ var titleEl = head.querySelector('.conversation-work-run-title');
11302
+ var nextTitle = workRunTitle(run);
11303
+ if (titleEl && titleEl.textContent !== nextTitle) titleEl.textContent = nextTitle;
11304
+ var statusEl = head.querySelector('.conversation-work-run-status');
11305
+ if (statusEl && statusEl.textContent !== String(run.status || '')) statusEl.textContent = String(run.status || '');
11306
+ // Keep the selector identical to workRunChangeBadge(). The old
11307
+ // `.conversation-work-run-change-badge` typo never found the existing
11308
+ // node, so every expand/collapse refresh inserted another review badge.
11309
+ var badgeHolder = head.querySelector('.conversation-work-change-badge');
11310
+ var nextBadge = workRunChangeBadge(run);
11311
+ if (nextBadge) {
11312
+ if (badgeHolder) {
11313
+ if (badgeHolder.getAttribute('data-badge-key') !== nextBadge) {
11314
+ var badgeShell = document.createElement('span');
11315
+ badgeShell.innerHTML = nextBadge;
11316
+ var replacementBadge = badgeShell.firstChild;
11317
+ replacementBadge.setAttribute('data-badge-key', nextBadge);
11318
+ badgeHolder.parentNode.replaceChild(replacementBadge, badgeHolder);
11319
+ }
11320
+ } else {
11321
+ var badgeShellNew = document.createElement('span');
11322
+ badgeShellNew.innerHTML = nextBadge;
11323
+ var badgeNode = badgeShellNew.firstChild;
11324
+ if (badgeNode) {
11325
+ badgeNode.setAttribute('data-badge-key', nextBadge);
11326
+ head.insertBefore(badgeNode, statusEl || null);
11327
+ }
11328
+ }
11329
+ } else if (badgeHolder) {
11330
+ badgeHolder.parentNode.removeChild(badgeHolder);
11331
+ }
11332
+ var body = element.querySelector('.conversation-work-run-body');
11333
+ if (body) updateWorkRunEventsContainer(run, body, expanded);
11334
+ else element.insertAdjacentHTML('beforeend', '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>');
11335
+ }
10671
11336
  renderPendingGuideMessages(run.target, {});
10672
11337
  var wrapper = element.closest ? element.closest('.work-run-message') : null;
10673
11338
  if (wrapper) {
@@ -10698,9 +11363,16 @@ function updateConversationWorkRunElement(run, element) {
10698
11363
  var refreshedKey = refreshedParentKey ? refreshedParentKey + '::' + refreshedDetailKey : '';
10699
11364
  if (activityOpenStates[refreshedKey] !== undefined) refreshedActivities[refreshedIndex].open = activityOpenStates[refreshedKey];
10700
11365
  }
10701
- autoScrollIfAtBottom();
10702
- return element;
10703
- }
11366
+ if (chatEl) {
11367
+ if (wasAtBottom) {
11368
+ _chatShouldAutoScroll = true;
11369
+ chatEl.scrollTop = chatEl.scrollHeight;
11370
+ } else {
11371
+ chatEl.scrollTop = scrollTopBefore;
11372
+ }
11373
+ }
11374
+ return element;
11375
+ }
10704
11376
 
10705
11377
  function updateConversationWorkRunDuration(run) {
10706
11378
  var element = findWorkRunElement(run);
@@ -10933,7 +11605,8 @@ function conversationWorkUiState(conversationId, workspaceId) {
10933
11605
  finalResponseMsg: null,
10934
11606
  finalResponseText: '',
10935
11607
  finalResponseAt: 0,
10936
- pendingWorkReview: null
11608
+ pendingWorkReview: null,
11609
+ workReviews: []
10937
11610
  };
10938
11611
  }
10939
11612
  return state.agentWorkUiByConversation[id];
@@ -10951,7 +11624,8 @@ function resetConversationWorkUi(conversationId, keepCompleted, workspaceId) {
10951
11624
  finalResponseMsg: keepCompleted ? prior.finalResponseMsg : null,
10952
11625
  finalResponseText: keepCompleted ? prior.finalResponseText : '',
10953
11626
  finalResponseAt: keepCompleted ? prior.finalResponseAt : 0,
10954
- pendingWorkReview: keepCompleted ? prior.pendingWorkReview : null
11627
+ pendingWorkReview: keepCompleted ? prior.pendingWorkReview : null,
11628
+ workReviews: keepCompleted ? (prior.workReviews || []).slice() : []
10955
11629
  };
10956
11630
  return state.agentWorkUiByConversation[id];
10957
11631
  }
@@ -11233,9 +11907,12 @@ function renderChatMessages(messages, target) {
11233
11907
  if (!orphanRoles.user && !orphanRoles.assistant) renderOwnedWorkRun(orphanRun, true);
11234
11908
  }
11235
11909
  }
11236
- var pendingWorkReview = conversationWorkUiState(renderConversationId).pendingWorkReview;
11910
+ var priorWorkUi = conversationWorkUiState(renderConversationId);
11911
+ var pendingWorkReview = priorWorkUi.pendingWorkReview;
11912
+ var retainedWorkReviews = Array.isArray(priorWorkUi.workReviews) ? priorWorkUi.workReviews.slice() : [];
11237
11913
  resetConversationWorkUi(renderConversationId, false);
11238
11914
  conversationWorkUiState(renderConversationId).pendingWorkReview = pendingWorkReview;
11915
+ conversationWorkUiState(renderConversationId).workReviews = retainedWorkReviews;
11239
11916
  if (!normalizedMessages.length) {
11240
11917
  persistedRuns.forEach(function(run) { renderOwnedWorkRun(run, true); });
11241
11918
  var pendingGuideCount = renderPendingGuideMessages(renderTarget, persistedGuideIds);
@@ -11279,7 +11956,10 @@ function renderChatMessages(messages, target) {
11279
11956
  finishToolBatch(renderConversationId);
11280
11957
  persistedRuns.forEach(function(run) { renderOwnedWorkRun(run, true); });
11281
11958
  renderPendingGuideMessages(renderTarget, persistedGuideIds);
11282
- if (pendingWorkReview) addWorkReview(pendingWorkReview);
11959
+ retainedWorkReviews.forEach(function(item) {
11960
+ addWorkReview(item.diffs || item, item.runId || '');
11961
+ });
11962
+ if (pendingWorkReview) addWorkReview(pendingWorkReview.diffs || pendingWorkReview, pendingWorkReview.runId || '');
11283
11963
  renderConversationBranchPagers();
11284
11964
  renderAutoRouteRatingControls();
11285
11965
  if (typeof renderPendingOptionsInChat === 'function') renderPendingOptionsInChat(state.pendingOptions || []);
@@ -11531,6 +12211,7 @@ function renderAgentWorkEvent(event) {
11531
12211
  var eventQueueTarget = { workspaceId: eventWorkspaceId || runtimeWorkspaceId(''), conversationId: eventConversationId };
11532
12212
  state.backendQueue = setBackendQueueForTarget(event.queue || { steering: [], followUp: [] }, eventQueueTarget);
11533
12213
  if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, eventQueueTarget);
12214
+ syncQueueItemsFromSnapshot(event, eventQueueTarget);
11534
12215
  if (window.renderInputStack) window.renderInputStack();
11535
12216
  } else {
11536
12217
  return;
@@ -11588,6 +12269,9 @@ function appendAgentWorkEvent(event) {
11588
12269
  eventRuntimeKey(event);
11589
12270
  var active = isActiveConversationTarget(target);
11590
12271
  var visibleRuntimeBranch = active && isViewingRuntimeConversationBranch(target);
12272
+ // 模型回退不是隐藏的参数回退:活跃对话发生回退时,把输入框下方的
12273
+ // 模型选择区同步为实际生效的模型。
12274
+ if (active && event.fallback && event.fallback.to) applyModelFallbackToInputSurface(event);
11591
12275
  if (active) markConversationTracked(id, state.conversationTrackMs || 300000, workspaceId);
11592
12276
  if (active || isConversationTracked(id, workspaceId)) cacheAgentWorkEvent(event);
11593
12277
  var pendingProvisionalStop = (event.type === 'start' || event.status === 'running')
@@ -11756,6 +12440,7 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
11756
12440
  state.backendQueue = setBackendQueueForTarget(s.queued, snapshotTarget);
11757
12441
  if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, snapshotTarget);
11758
12442
  }
12443
+ syncQueueItemsFromSnapshot(s, snapshotTarget);
11759
12444
  if (s && Array.isArray(s.workEvents)) {
11760
12445
  var eventConversationId = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
11761
12446
  if (!state.agentWorkEventsByConversation) state.agentWorkEventsByConversation = {};
@@ -11980,6 +12665,7 @@ window.optionManualSubmitted = function(element) {
11980
12665
 
11981
12666
  // === Working State ===
11982
12667
  function setWorking(working) {
12668
+ var changed = state.isWorking !== working;
11983
12669
  state.isWorking = working;
11984
12670
  state.activeAgentRunning = working;
11985
12671
  var btn = els['submit-btn'];
@@ -11988,13 +12674,13 @@ function setWorking(working) {
11988
12674
  var ws = els['ws-thumb'];
11989
12675
  if (working) {
11990
12676
  if (btn) btn.classList.add('working');
11991
- if (input) input.classList.add('working-glow');
12677
+ if (input) input.classList.add('working-glow', 'marquee-border');
11992
12678
  if (title) title.classList.add('animate');
11993
12679
  if (ws) ws.classList.add('marquee-border');
11994
- renderConversations();
12680
+ if (changed) renderConversations();
11995
12681
  } else {
11996
12682
  if (btn) btn.classList.remove('working');
11997
- if (input) input.classList.remove('working-glow');
12683
+ if (input) input.classList.remove('working-glow', 'marquee-border');
11998
12684
  if (title) title.classList.remove('animate');
11999
12685
  if (ws) ws.classList.remove('marquee-border');
12000
12686
  }
@@ -12534,6 +13220,7 @@ window.closeModelSelectMenu = function(options) {
12534
13220
  if (shell) shell.classList.remove('open');
12535
13221
  if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
12536
13222
  if (button) button.setAttribute('aria-expanded', 'false');
13223
+ window.setLiquidSidebarGestureLock('popup:model-select', false);
12537
13224
  if (options && options.focus && button) button.focus();
12538
13225
  };
12539
13226
 
@@ -12550,6 +13237,7 @@ window.toggleModelSelectMenu = function(forceOpen) {
12550
13237
  }
12551
13238
  if (open) document.querySelectorAll('.newmark-select-shell.open').forEach(function(item) { window.closeNewmarkSelect(item); });
12552
13239
  shell.classList.add('open');
13240
+ window.setLiquidSidebarGestureLock('popup:model-select', true);
12553
13241
  button.setAttribute('aria-expanded', 'true');
12554
13242
  if (open) {
12555
13243
  if (menu && typeof menu.showPopover === 'function' && !menu.matches(':popover-open')) menu.showPopover();
@@ -12577,6 +13265,7 @@ window.closeConversationActionMenu = function(options) {
12577
13265
  if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
12578
13266
  document.querySelectorAll('.conv-more-btn[aria-expanded="true"]').forEach(function(item) { item.setAttribute('aria-expanded', 'false'); });
12579
13267
  state.conversationActionAnchor = null;
13268
+ window.setLiquidSidebarGestureLock('popup:conversation-action', false);
12580
13269
  if (options && options.focus && anchor && anchor.focus) anchor.focus();
12581
13270
  };
12582
13271
 
@@ -12632,6 +13321,7 @@ window.openConversationActionMenu = function(anchor, conversationId, pos) {
12632
13321
  document.querySelectorAll('.conv-more-btn[aria-expanded="true"]').forEach(function(item) { item.setAttribute('aria-expanded', 'false'); });
12633
13322
  if (moreBtn) moreBtn.setAttribute('aria-expanded', 'true');
12634
13323
  state.conversationActionAnchor = anchor || moreBtn || null;
13324
+ window.setLiquidSidebarGestureLock('popup:conversation-action', true);
12635
13325
  if (typeof menu.showPopover === 'function' && !menu.matches(':popover-open')) menu.showPopover();
12636
13326
  requestAnimationFrame(function() {
12637
13327
  window.positionConversationActionMenu(state.conversationActionAnchor || moreBtn, menu, pos);
@@ -12695,6 +13385,967 @@ window.handleModelSelectButtonKey = function(event) {
12695
13385
  }
12696
13386
  };
12697
13387
 
13388
+ // 后端回退信号携带的是纯模型名(如 gpt-4o-mini),把名称映射回前端
13389
+ // 选择器的部署值(deployment:providerId:modelName),使选择区能直接选中。
13390
+ function modelDeploymentValueForName(name, providerId) {
13391
+ var modelName = String(name || '').trim();
13392
+ if (!modelName) return '';
13393
+ var providers = Array.isArray(state.providers) ? state.providers : [];
13394
+ var match = null;
13395
+ for (var pi = 0; pi < providers.length && !match; pi++) {
13396
+ var provider = providers[pi];
13397
+ if (providerId && String(provider.id || provider.name || '') !== String(providerId)) continue;
13398
+ var models = Array.isArray(provider.models) ? provider.models : [];
13399
+ for (var mi = 0; mi < models.length && !match; mi++) {
13400
+ var entryName = typeof models[mi] === 'string' ? models[mi] : (models[mi] && models[mi].name || '');
13401
+ if (entryName === modelName) match = { provider: provider, name: entryName };
13402
+ }
13403
+ }
13404
+ if (!match) return modelName;
13405
+ var providerIdValue = String(match.provider.id || match.provider.name || '');
13406
+ return 'deployment:' + encodeURIComponent(providerIdValue) + ':' + encodeURIComponent(match.name);
13407
+ }
13408
+
13409
+ // 不可用模型回退同步输入框下方选择区:固定模型选择被回退时,选择区
13410
+ // 显示实际生效的模型;Auto 模式的路由切换不污染选择区(保持 Auto 显示)。
13411
+ function applyModelFallbackToInputSurface(event) {
13412
+ var f = event && event.fallback;
13413
+ if (!f || !f.to) return;
13414
+ if (String(state.model || '') === 'auto') return;
13415
+ var nextValue = modelDeploymentValueForName(f.to, f.providerId || '');
13416
+ if (!nextValue || String(state.model || '') === nextValue) return;
13417
+ state.model = nextValue;
13418
+ state.modelLabel = '';
13419
+ // 不预置 _syncedModel:下一次发送前的 syncConversationExecutionState 会把
13420
+ // 回退模型正式同步到后端 host agent(并持久化为该对话的首选模型)。
13421
+ window.refreshModelSelect();
13422
+ window.syncModelSelectSurface();
13423
+ }
13424
+
13425
+ var liquidSidebarGestureLocks = new Set();
13426
+ window.setLiquidSidebarGestureLock = function(token, locked) {
13427
+ if (locked) liquidSidebarGestureLocks.add(String(token));
13428
+ else liquidSidebarGestureLocks.delete(String(token));
13429
+ document.body.classList.toggle('liquid-sidebar-gesture-locked', liquidSidebarGestureLocks.size > 0);
13430
+ };
13431
+ window.liquidSidebarGesturesLocked = function() { return liquidSidebarGestureLocks.size > 0; };
13432
+
13433
+ // One moving glass capsule per select menu. It starts at the existing
13434
+ // selection, fades in, follows pointer movement across options, and commits
13435
+ // the landing option on release. Static options retain their color capsule.
13436
+ var liquidBackdropFramePromise = null;
13437
+ var liquidBackdropCachedFrame = null;
13438
+ var liquidBackdropPrimeTimer = 0;
13439
+ window.__liquidBackdropPrimeReady = false;
13440
+ function decodeLiquidBackdropFrame(frame) {
13441
+ if (!frame) return Promise.resolve(null);
13442
+ if (frame.bitmap) return Promise.resolve(frame);
13443
+ var bytes = frame.bytes;
13444
+ if (bytes && Array.isArray(bytes.data)) bytes = new Uint8Array(bytes.data);
13445
+ var blobPromise = bytes
13446
+ ? Promise.resolve(new Blob([bytes], { type: frame.mimeType || 'image/jpeg' }))
13447
+ : (frame.dataUrl ? fetch(frame.dataUrl).then(function(response) { return response.blob(); }) : Promise.resolve(null));
13448
+ return blobPromise.then(function(blob) {
13449
+ if (!blob) return null;
13450
+ return createImageBitmap(blob);
13451
+ }).then(function(bitmap) {
13452
+ if (!bitmap) return null;
13453
+ frame.bitmap = bitmap;
13454
+ frame.id = frame.id || String(Date.now()) + ':' + String(frame.width || bitmap.width) + 'x' + String(frame.height || bitmap.height);
13455
+ frame.capturedAt = performance.now();
13456
+ return frame;
13457
+ });
13458
+ }
13459
+ function captureLiquidBackdropFrame(options) {
13460
+ if (!window.api || typeof window.api.captureLiquidBackdrop !== 'function') return Promise.resolve(null);
13461
+ options = options || {};
13462
+ var maxAge = Number.isFinite(options.maxAge) ? options.maxAge : 5000;
13463
+ if (!options.force && liquidBackdropCachedFrame && performance.now() - liquidBackdropCachedFrame.capturedAt <= maxAge) {
13464
+ return Promise.resolve(liquidBackdropCachedFrame);
13465
+ }
13466
+ if (!liquidBackdropFramePromise) {
13467
+ window.__liquidBackdropPrimeReady = false;
13468
+ liquidBackdropFramePromise = window.api.captureLiquidBackdrop({ width: window.innerWidth, height: window.innerHeight })
13469
+ .then(decodeLiquidBackdropFrame)
13470
+ .then(function(frame) {
13471
+ if (frame) {
13472
+ var previous = liquidBackdropCachedFrame;
13473
+ liquidBackdropCachedFrame = frame;
13474
+ prewarmKyantRenderer(frame);
13475
+ window.__liquidBackdropPrimeReady = true;
13476
+ if (previous && previous !== frame && previous.bitmap && previous.bitmap.close) previous.bitmap.close();
13477
+ }
13478
+ return frame;
13479
+ })
13480
+ .catch(function(error) {
13481
+ window.__liquidErrors = window.__liquidErrors || [];
13482
+ window.__liquidErrors.push(String(error && (error.stack || error.message) || error));
13483
+ console.warn('[KyantGlass] backdrop capture failed:', error);
13484
+ return liquidBackdropCachedFrame;
13485
+ })
13486
+ .finally(function() { liquidBackdropFramePromise = null; });
13487
+ }
13488
+ return liquidBackdropFramePromise;
13489
+ }
13490
+
13491
+ function scheduleLiquidBackdropPrime() {
13492
+ if (liquidBackdropPrimeTimer || (liquidBackdropCachedFrame && performance.now() - liquidBackdropCachedFrame.capturedAt < 5000)) return;
13493
+ liquidBackdropPrimeTimer = setTimeout(function() {
13494
+ liquidBackdropPrimeTimer = 0;
13495
+ captureLiquidBackdropFrame({ maxAge: 5000 });
13496
+ }, liquidBackdropCachedFrame ? 220 : 48);
13497
+ }
13498
+
13499
+ if (window.requestIdleCallback) {
13500
+ window.requestIdleCallback(function() { captureLiquidBackdropFrame({ maxAge: 5000 }); }, { timeout: 900 });
13501
+ } else {
13502
+ setTimeout(function() { captureLiquidBackdropFrame({ maxAge: 5000 }); }, 600);
13503
+ }
13504
+
13505
+ document.addEventListener('pointerover', function(event) {
13506
+ 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;
13507
+ if (target) scheduleLiquidBackdropPrime();
13508
+ }, { passive: true });
13509
+ document.addEventListener('focusin', function(event) {
13510
+ 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;
13511
+ if (target) scheduleLiquidBackdropPrime();
13512
+ });
13513
+
13514
+ function compileLiquidShader(gl, type, source) {
13515
+ var shader = gl.createShader(type);
13516
+ gl.shaderSource(shader, source);
13517
+ gl.compileShader(shader);
13518
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
13519
+ console.error('[KyantGlass] shader compile failed:', gl.getShaderInfoLog(shader));
13520
+ gl.deleteShader(shader);
13521
+ return null;
13522
+ }
13523
+ return shader;
13524
+ }
13525
+
13526
+ function liquidGlassCssNumber(name, fallback) {
13527
+ var value = parseFloat(getComputedStyle(document.documentElement).getPropertyValue(name));
13528
+ return Number.isFinite(value) ? value : fallback;
13529
+ }
13530
+
13531
+ var liquidKyantRendererPool = [];
13532
+ var liquidKyantVertexSource = '#version 300 es\n' +
13533
+ 'in vec2 p; out vec2 uv; void main(){uv=p*.5+.5;gl_Position=vec4(p,0.,1.);}';
13534
+ var liquidKyantFragmentSource = '#version 300 es\nprecision highp float;\n' +
13535
+ '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' +
13536
+ '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' +
13537
+ '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' +
13538
+ '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' +
13539
+ 'float circleMap(float x){x=clamp(x,0.,1.);return 1.-sqrt(max(0.,1.-x*x));}\n' +
13540
+ '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' +
13541
+ '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);}';
13542
+
13543
+ function createKyantLiquidRenderer() {
13544
+ var canvas = document.createElement('canvas');
13545
+ var displayCanvas = document.createElement('canvas');
13546
+ displayCanvas.className = 'liquid-selection-canvas';
13547
+ var displayContext = displayCanvas.getContext('2d', { alpha:true });
13548
+ if (!displayContext) return null;
13549
+ var gl = canvas.getContext('webgl2', { alpha:true, antialias:false, premultipliedAlpha:true, powerPreference:'high-performance' });
13550
+ if (!gl) return null;
13551
+ var vs = compileLiquidShader(gl, gl.VERTEX_SHADER, liquidKyantVertexSource);
13552
+ var fs = compileLiquidShader(gl, gl.FRAGMENT_SHADER, liquidKyantFragmentSource);
13553
+ if (!vs || !fs) return null;
13554
+ var program = gl.createProgram();
13555
+ gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program);
13556
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
13557
+ console.error('[KyantGlass] program link failed:', gl.getProgramInfoLog(program));
13558
+ return null;
13559
+ }
13560
+ gl.deleteShader(vs);
13561
+ gl.deleteShader(fs);
13562
+ var buffer = gl.createBuffer();
13563
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
13564
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]), gl.STATIC_DRAW);
13565
+ var location = gl.getAttribLocation(program, 'p');
13566
+ gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location,2,gl.FLOAT,false,0,0);
13567
+ var texture = gl.createTexture();
13568
+ gl.bindTexture(gl.TEXTURE_2D, texture);
13569
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
13570
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
13571
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
13572
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
13573
+ var uniforms = {
13574
+ content: gl.getUniformLocation(program,'content'),
13575
+ size: gl.getUniformLocation(program,'size'),
13576
+ viewport: gl.getUniformLocation(program,'viewport'),
13577
+ origin: gl.getUniformLocation(program,'origin'),
13578
+ cornerRadii: gl.getUniformLocation(program,'cornerRadii'),
13579
+ refractionHeight: gl.getUniformLocation(program,'refractionHeight'),
13580
+ refractionAmount: gl.getUniformLocation(program,'refractionAmount'),
13581
+ depthEffect: gl.getUniformLocation(program,'depthEffect'),
13582
+ chromaticAberration: gl.getUniformLocation(program,'chromaticAberration')
13583
+ };
13584
+ gl.useProgram(program);
13585
+ gl.uniform1i(uniforms.content, 0);
13586
+ var renderer = {
13587
+ canvas: canvas,
13588
+ displayCanvas: displayCanvas,
13589
+ displayContext: displayContext,
13590
+ gl: gl,
13591
+ program: program,
13592
+ texture: texture,
13593
+ uniforms: uniforms,
13594
+ owner: null,
13595
+ textureReady: false,
13596
+ generation: 0,
13597
+ renderFrame: 0,
13598
+ frameId: '',
13599
+ lastGeometry: null,
13600
+ pendingGeometry: null,
13601
+ uniformState: Object.create(null),
13602
+ invalid: false
13603
+ };
13604
+ canvas.addEventListener('webglcontextlost', function(event) {
13605
+ event.preventDefault();
13606
+ renderer.invalid = true;
13607
+ renderer.textureReady = false;
13608
+ if (renderer.renderFrame) cancelAnimationFrame(renderer.renderFrame);
13609
+ renderer.renderFrame = 0;
13610
+ liquidKyantRendererPool = liquidKyantRendererPool.filter(function(item) { return item !== renderer; });
13611
+ });
13612
+ return renderer;
13613
+ }
13614
+
13615
+ function acquireKyantLiquidRenderer(float) {
13616
+ var renderer = liquidKyantRendererPool.find(function(item) {
13617
+ return !item.invalid && (!item.owner || !item.owner.isConnected);
13618
+ });
13619
+ if (!renderer) {
13620
+ renderer = createKyantLiquidRenderer();
13621
+ if (!renderer) return null;
13622
+ liquidKyantRendererPool.push(renderer);
13623
+ }
13624
+ renderer.owner = float;
13625
+ renderer.generation += 1;
13626
+ if (renderer.renderFrame) cancelAnimationFrame(renderer.renderFrame);
13627
+ renderer.renderFrame = 0;
13628
+ renderer.displayCanvas.remove();
13629
+ renderer.lastGeometry = null;
13630
+ renderer.pendingGeometry = null;
13631
+ return renderer;
13632
+ }
13633
+
13634
+ function uploadKyantLiquidFrame(renderer, frame) {
13635
+ if (!renderer || !frame || !frame.bitmap || renderer.frameId === frame.id) return false;
13636
+ var gl = renderer.gl;
13637
+ gl.bindTexture(gl.TEXTURE_2D, renderer.texture);
13638
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
13639
+ gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,frame.bitmap);
13640
+ renderer.textureReady = true;
13641
+ renderer.frameId = frame.id;
13642
+ return true;
13643
+ }
13644
+
13645
+ function prewarmKyantRenderer(frame) {
13646
+ if (!frame || !frame.bitmap) return;
13647
+ var renderer = liquidKyantRendererPool.find(function(item) { return !item.invalid && !item.owner; });
13648
+ if (!renderer) {
13649
+ if (liquidKyantRendererPool.some(function(item) { return !item.invalid; })) return;
13650
+ renderer = createKyantLiquidRenderer();
13651
+ if (!renderer) return;
13652
+ liquidKyantRendererPool.push(renderer);
13653
+ }
13654
+ uploadKyantLiquidFrame(renderer, frame);
13655
+ }
13656
+
13657
+ function attachKyantLiquidRenderer(float, frame) {
13658
+ var renderer = float._kyantRendererRef;
13659
+ if (!renderer || renderer.owner !== float) renderer = acquireKyantLiquidRenderer(float);
13660
+ if (!renderer) { float.dataset.kyantRenderer = 'webgl2-unavailable'; return; }
13661
+ float._kyantRendererRef = renderer;
13662
+ var canvas = renderer.canvas;
13663
+ var displayCanvas = renderer.displayCanvas;
13664
+ var displayContext = renderer.displayContext;
13665
+ var gl = renderer.gl;
13666
+ var program = renderer.program;
13667
+ var texture = renderer.texture;
13668
+ var uniforms = renderer.uniforms;
13669
+ var generation = renderer.generation;
13670
+ float.dataset.kyantRenderer = 'webgl2';
13671
+ float._kyantTextureReady = false;
13672
+ float._kyantDrawReady = false;
13673
+ delete float.dataset.kyantDrawReady;
13674
+ float._renderKyantGlass = function(geometry, immediate) {
13675
+ renderer.pendingGeometry = geometry || null;
13676
+ if (!renderer.textureReady || renderer.owner !== float || !float.isConnected) return;
13677
+ if (immediate && renderer.renderFrame) {
13678
+ cancelAnimationFrame(renderer.renderFrame);
13679
+ renderer.renderFrame = 0;
13680
+ }
13681
+ if (renderer.renderFrame) return;
13682
+ var draw = function() {
13683
+ renderer.renderFrame = 0;
13684
+ if (!renderer.textureReady || renderer.owner !== float || !float.isConnected) return;
13685
+ var geometry = renderer.pendingGeometry;
13686
+ renderer.pendingGeometry = null;
13687
+ var style = geometry ? null : getComputedStyle(float);
13688
+ var width = geometry ? geometry.width : (parseFloat(style.width) || float.offsetWidth || 1);
13689
+ var height = geometry ? geometry.height : (parseFloat(style.height) || float.offsetHeight || 1);
13690
+ var left = geometry ? geometry.left : (parseFloat(style.left) || 0);
13691
+ var top = geometry ? geometry.top : (parseFloat(style.top) || 0);
13692
+ var dpr = window.devicePixelRatio || 1;
13693
+ var renderScale = Math.min(dpr, 1.5);
13694
+ var pixelWidth = Math.max(1, Math.round(width*renderScale));
13695
+ var pixelHeight = Math.max(1, Math.round(height*renderScale));
13696
+ var geometryKey = [left.toFixed(2),top.toFixed(2),width.toFixed(2),height.toFixed(2),pixelWidth,pixelHeight].join(':');
13697
+ if (renderer.lastGeometry === geometryKey) return;
13698
+ renderer.lastGeometry = geometryKey;
13699
+ if (canvas.width !== pixelWidth) canvas.width = pixelWidth;
13700
+ if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
13701
+ if (displayCanvas.width !== pixelWidth) displayCanvas.width = pixelWidth;
13702
+ if (displayCanvas.height !== pixelHeight) displayCanvas.height = pixelHeight;
13703
+ gl.viewport(0,0,pixelWidth,pixelHeight);
13704
+ gl.useProgram(program);
13705
+ function uniformChanged(key, values, apply) {
13706
+ var prior = renderer.uniformState[key];
13707
+ if (prior && prior.length === values.length && values.every(function(value,index) { return prior[index] === value; })) return;
13708
+ renderer.uniformState[key] = values;
13709
+ apply();
13710
+ }
13711
+ uniformChanged('size',[width,height],function() { gl.uniform2f(uniforms.size,width,height); });
13712
+ uniformChanged('viewport',[window.innerWidth,window.innerHeight],function() { gl.uniform2f(uniforms.viewport,window.innerWidth,window.innerHeight); });
13713
+ uniformChanged('origin',[left,top],function() { gl.uniform2f(uniforms.origin,left,top); });
13714
+ var capsuleRadius = Math.min(width,height)*.5;
13715
+ uniformChanged('cornerRadii',[capsuleRadius],function() { gl.uniform4f(uniforms.cornerRadii,capsuleRadius,capsuleRadius,capsuleRadius,capsuleRadius); });
13716
+ var configuredHeight = liquidGlassCssNumber('--liquid-interaction-edge', 6);
13717
+ var configuredAmount = liquidGlassCssNumber('--liquid-float-refraction-amount', 24);
13718
+ var configuredChromatic = liquidGlassCssNumber('--liquid-float-chromatic-aberration', 1);
13719
+ var refractionHeight = Math.max(0.001,Math.min(configuredHeight,height*.48));
13720
+ uniformChanged('refractionHeight',[refractionHeight],function() { gl.uniform1f(uniforms.refractionHeight,refractionHeight); });
13721
+ uniformChanged('refractionAmount',[-configuredAmount],function() { gl.uniform1f(uniforms.refractionAmount,-configuredAmount); });
13722
+ uniformChanged('depthEffect',[1],function() { gl.uniform1f(uniforms.depthEffect,1); });
13723
+ uniformChanged('chromaticAberration',[configuredChromatic],function() { gl.uniform1f(uniforms.chromaticAberration,configuredChromatic); });
13724
+ gl.drawArrays(gl.TRIANGLES,0,6);
13725
+ displayContext.clearRect(0,0,pixelWidth,pixelHeight);
13726
+ displayContext.drawImage(canvas,0,0);
13727
+ float._kyantDrawReady = true;
13728
+ float.dataset.kyantDrawReady = 'true';
13729
+ if (displayCanvas.parentNode !== float) float.appendChild(displayCanvas);
13730
+ };
13731
+ if (immediate) draw();
13732
+ else renderer.renderFrame = requestAnimationFrame(draw);
13733
+ };
13734
+ float._releaseKyantGlass = function() {
13735
+ if (renderer.owner !== float) return;
13736
+ renderer.generation += 1;
13737
+ renderer.owner = null;
13738
+ if (renderer.renderFrame) cancelAnimationFrame(renderer.renderFrame);
13739
+ renderer.renderFrame = 0;
13740
+ renderer.lastGeometry = null;
13741
+ renderer.pendingGeometry = null;
13742
+ displayCanvas.remove();
13743
+ delete float._kyantRendererRef;
13744
+ };
13745
+ if (frame && frame.bitmap && renderer.owner === float && renderer.generation === generation && float.isConnected) {
13746
+ uploadKyantLiquidFrame(renderer, frame);
13747
+ float._kyantTextureReady = true;
13748
+ if (float.style.width && float.style.height) float._renderKyantGlass(null, true);
13749
+ } else if (renderer.textureReady) {
13750
+ float._kyantTextureReady = true;
13751
+ if (float.style.width && float.style.height) float._renderKyantGlass(null, true);
13752
+ } else {
13753
+ float.dataset.kyantRenderer = 'waiting-backdrop';
13754
+ }
13755
+ }
13756
+
13757
+ function mountLiquidViewportFloat(float) {
13758
+ if (liquidBackdropPrimeTimer) clearTimeout(liquidBackdropPrimeTimer);
13759
+ liquidBackdropPrimeTimer = 0;
13760
+ float.classList.add('liquid-selection-viewport');
13761
+ float.setAttribute('popover', 'manual');
13762
+ document.body.appendChild(float);
13763
+ startLiquidMotionTracking(float);
13764
+ attachKyantLiquidRenderer(float, liquidBackdropCachedFrame);
13765
+ }
13766
+
13767
+ function showLiquidViewportFloat(float) {
13768
+ try { float.showPopover(); } catch (_) {}
13769
+ }
13770
+
13771
+ function keepLiquidFloatExpansionInsideViewport(float, rect) {
13772
+ if (!float || !rect) return;
13773
+ var edge = 28;
13774
+ float.style.transformOrigin = rect.left < edge ? 'left center' : (rect.right > window.innerWidth - edge ? 'right center' : 'center');
13775
+ }
13776
+
13777
+ function liquidInteractionEdgePx() {
13778
+ return liquidGlassCssNumber('--liquid-interaction-edge', 6);
13779
+ }
13780
+
13781
+ function startLiquidMotionTracking(float) {
13782
+ if (!float || float._liquidMotionFrame) return;
13783
+ var lastTime = 0;
13784
+ var lastX = 0;
13785
+ var lastY = 0;
13786
+ var velocityX = 0;
13787
+ var velocityY = 0;
13788
+ var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13789
+ function frame(now) {
13790
+ if (!float || !float.isConnected) {
13791
+ if (float) float._liquidMotionFrame = 0;
13792
+ return;
13793
+ }
13794
+ var rect = float.getBoundingClientRect();
13795
+ var left = rect.left;
13796
+ var top = rect.top;
13797
+ var width = rect.width;
13798
+ var height = rect.height;
13799
+ var centerX = left + width * .5;
13800
+ var centerY = top + height * .5;
13801
+ var moved = lastTime > 0 && (Math.abs(centerX - lastX) > .01 || Math.abs(centerY - lastY) > .01);
13802
+ if (lastTime > 0) {
13803
+ var seconds = Math.max(.001, Math.min(.05, (now - lastTime) / 1000));
13804
+ var instantaneousX = (centerX - lastX) / seconds;
13805
+ var instantaneousY = (centerY - lastY) / seconds;
13806
+ velocityX = velocityX * .45 + instantaneousX * .55;
13807
+ velocityY = velocityY * .45 + instantaneousY * .55;
13808
+ }
13809
+ lastTime = now;
13810
+ lastX = centerX;
13811
+ lastY = centerY;
13812
+ var speed = reducedMotion ? 0 : Math.hypot(velocityX, velocityY);
13813
+ var amount = Math.max(0, Math.min(1, (speed - 8) / 700));
13814
+ var angle = speed > 1 ? Math.atan2(velocityY, velocityX) : 0;
13815
+ float.style.setProperty('--liquid-motion-angle', angle + 'rad');
13816
+ float.style.setProperty('--liquid-motion-stretch', String(1 + amount * .075));
13817
+ float.style.setProperty('--liquid-motion-squash', String(1 - amount * .035));
13818
+ if (window.__liquidPerfCounts) float.dataset.liquidMotionAmount = amount.toFixed(4);
13819
+ if (moved && float._renderKyantGlass) float._renderKyantGlass({ left:left, top:top, width:width, height:height }, true);
13820
+ float._liquidMotionFrame = requestAnimationFrame(frame);
13821
+ }
13822
+ float._liquidMotionFrame = requestAnimationFrame(frame);
13823
+ }
13824
+
13825
+ function stopLiquidMotionTracking(float) {
13826
+ if (!float) return;
13827
+ if (float._liquidArrivalFrame) cancelAnimationFrame(float._liquidArrivalFrame);
13828
+ float._liquidArrivalFrame = 0;
13829
+ if (float._liquidMotionFrame) cancelAnimationFrame(float._liquidMotionFrame);
13830
+ float._liquidMotionFrame = 0;
13831
+ float.style.removeProperty('--liquid-motion-angle');
13832
+ float.style.removeProperty('--liquid-motion-stretch');
13833
+ float.style.removeProperty('--liquid-motion-squash');
13834
+ }
13835
+
13836
+ function waitForLiquidSelectionArrival(float, onArrive) {
13837
+ if (!float || !float.isConnected) { onArrive(); return; }
13838
+ var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13839
+ if (reducedMotion) { onArrive(); return; }
13840
+ if (float._liquidArrivalFrame) cancelAnimationFrame(float._liquidArrivalFrame);
13841
+ var startedAt = performance.now();
13842
+ function settled() {
13843
+ if (!float || !float.isConnected) return true;
13844
+ var target = float._liquidTargetRect;
13845
+ if (!target) return true;
13846
+ var style = getComputedStyle(float);
13847
+ var left = parseFloat(style.left) || 0;
13848
+ var top = parseFloat(style.top) || 0;
13849
+ var width = parseFloat(style.width) || 0;
13850
+ var height = parseFloat(style.height) || 0;
13851
+ return Math.abs(left - target.left) < .75 && Math.abs(top - target.top) < .75 &&
13852
+ Math.abs(width - target.width) < .75 && Math.abs(height - target.height) < .75 &&
13853
+ parseFloat(style.opacity || '0') >= .995;
13854
+ }
13855
+ function inspect() {
13856
+ if (settled() || performance.now() - startedAt > 300) {
13857
+ float._liquidArrivalFrame = 0;
13858
+ onArrive();
13859
+ return;
13860
+ }
13861
+ float._liquidArrivalFrame = requestAnimationFrame(inspect);
13862
+ }
13863
+ float._liquidArrivalFrame = requestAnimationFrame(inspect);
13864
+ }
13865
+
13866
+ function landLiquidSelectionFloat(float, cleanup, onComplete) {
13867
+ if (!float) { cleanup(); if (onComplete) onComplete(); return; }
13868
+ var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13869
+ if (!float.isConnected) { cleanup(); if (onComplete) onComplete(); return; }
13870
+ stopLiquidMotionTracking(float);
13871
+ float.classList.add('liquid-selection-landing');
13872
+ float._liquidLandingTimer = setTimeout(function() {
13873
+ cleanup();
13874
+ scheduleLiquidBackdropPrime();
13875
+ if (onComplete) onComplete();
13876
+ }, reducedMotion ? 0 : 120);
13877
+ }
13878
+
13879
+ function wireLiquidMenuInteractions(menu) {
13880
+ if (!menu || menu.dataset.liquidInteractions === 'true') return;
13881
+ menu.dataset.liquidInteractions = 'true';
13882
+ var pointer = null;
13883
+ var source = null;
13884
+ var target = null;
13885
+ var float = null;
13886
+ var generation = 0;
13887
+ var pressX = 0;
13888
+ var pressY = 0;
13889
+ var gestureMoved = false;
13890
+ function optionAt(event) {
13891
+ var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
13892
+ var node = hit && hit.closest ? hit.closest('button.model-select-menu-option, button.newmark-select-option') : null;
13893
+ return node && menu.contains(node) && !node.disabled ? node : null;
13894
+ }
13895
+ function position(option) {
13896
+ if (!option || !float) return;
13897
+ if (float.classList.contains('liquid-selection-viewport')) {
13898
+ var rect = option.getBoundingClientRect();
13899
+ var edge = liquidInteractionEdgePx();
13900
+ keepLiquidFloatExpansionInsideViewport(float, rect);
13901
+ float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
13902
+ float.style.left = (rect.left - edge) + 'px';
13903
+ float.style.top = (rect.top - edge) + 'px';
13904
+ float.style.width = (rect.width + edge * 2) + 'px';
13905
+ float.style.height = (rect.height + edge * 2) + 'px';
13906
+ if (float._renderKyantGlass) float._renderKyantGlass({
13907
+ left: rect.left - edge, top: rect.top - edge,
13908
+ width: rect.width + edge * 2, height: rect.height + edge * 2
13909
+ }, true);
13910
+ return;
13911
+ }
13912
+ var edge = liquidInteractionEdgePx();
13913
+ float.style.left = (option.offsetLeft - edge) + 'px';
13914
+ float.style.top = (option.offsetTop - edge) + 'px';
13915
+ float.style.width = (option.offsetWidth + edge * 2) + 'px';
13916
+ float.style.height = (option.offsetHeight + edge * 2) + 'px';
13917
+ }
13918
+ function positionAlongPointer(event) {
13919
+ if (!float || !event) return;
13920
+ var menuRect = menu.getBoundingClientRect();
13921
+ var clipTop = Math.max(0, menuRect.top + menu.clientTop + 7);
13922
+ var clipBottom = Math.min(window.innerHeight, menuRect.bottom - menu.clientTop - 7);
13923
+ var allOptions = Array.from(menu.querySelectorAll('button.model-select-menu-option, button.newmark-select-option'))
13924
+ .filter(function(option) { return !option.disabled && option.getClientRects().length; });
13925
+ var options = allOptions.filter(function(option) {
13926
+ var rect = option.getBoundingClientRect();
13927
+ return rect.bottom > clipTop && rect.top < clipBottom;
13928
+ });
13929
+ if (!options.length) return;
13930
+ var nearest = options[0];
13931
+ var nearestDistance = Infinity;
13932
+ options.forEach(function(option) {
13933
+ var rect = option.getBoundingClientRect();
13934
+ var distance = Math.abs(event.clientY - (rect.top + rect.height / 2));
13935
+ if (distance < nearestDistance) { nearest = option; nearestDistance = distance; }
13936
+ });
13937
+ target = nearest;
13938
+ var rect = nearest.getBoundingClientRect();
13939
+ var firstRect = options[0].getBoundingClientRect();
13940
+ var lastRect = options[options.length - 1].getBoundingClientRect();
13941
+ var minCenter = Math.min(firstRect.top + firstRect.height / 2, lastRect.top + lastRect.height / 2);
13942
+ var maxCenter = Math.max(firstRect.top + firstRect.height / 2, lastRect.top + lastRect.height / 2);
13943
+ if (maxCenter < minCenter) maxCenter = minCenter;
13944
+ var centerY = Math.max(minCenter, Math.min(maxCenter, event.clientY));
13945
+ var edge = liquidInteractionEdgePx();
13946
+ float.style.left = (rect.left - edge) + 'px';
13947
+ float.style.top = (centerY - rect.height / 2 - edge) + 'px';
13948
+ float.style.width = (rect.width + edge * 2) + 'px';
13949
+ float.style.height = (rect.height + edge * 2) + 'px';
13950
+ }
13951
+ function detachWindowTracking() {
13952
+ window.removeEventListener('pointermove', onPointerMove, true);
13953
+ window.removeEventListener('pointerup', onPointerUp, true);
13954
+ window.removeEventListener('pointercancel', onPointerCancel, true);
13955
+ }
13956
+ function clear() {
13957
+ detachWindowTracking();
13958
+ if (source) source.classList.remove('liquid-selection-source');
13959
+ stopLiquidMotionTracking(float);
13960
+ if (float && float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
13961
+ if (float && float._releaseKyantGlass) float._releaseKyantGlass();
13962
+ if (float && float.parentNode) float.parentNode.removeChild(float);
13963
+ generation += 1;
13964
+ pointer = source = target = float = null;
13965
+ }
13966
+ function onPointerMove(event) {
13967
+ if (pointer === null || event.pointerId !== pointer) return;
13968
+ if (!gestureMoved && Math.hypot(event.clientX - pressX, event.clientY - pressY) >= 4) {
13969
+ gestureMoved = true;
13970
+ }
13971
+ event.preventDefault();
13972
+ positionAlongPointer(event);
13973
+ }
13974
+ function onPointerUp(event) {
13975
+ if (pointer === null || event.pointerId !== pointer) return;
13976
+ var commit = target;
13977
+ detachWindowTracking();
13978
+ try { menu.releasePointerCapture(pointer); } catch (_) {}
13979
+ pointer = null;
13980
+ if (commit) position(commit);
13981
+ waitForLiquidSelectionArrival(float, function() {
13982
+ landLiquidSelectionFloat(float, clear, function() {
13983
+ if (commit && commit.isConnected) commit.click();
13984
+ });
13985
+ });
13986
+ }
13987
+ function onPointerCancel(event) {
13988
+ if (pointer === null || event.pointerId === pointer) clear();
13989
+ }
13990
+ menu.addEventListener('pointerdown', function(event) {
13991
+ var pressed = optionAt(event);
13992
+ if (!pressed || event.button !== 0) return;
13993
+ event.preventDefault();
13994
+ target = pressed;
13995
+ pointer = event.pointerId;
13996
+ pressX = event.clientX;
13997
+ pressY = event.clientY;
13998
+ gestureMoved = false;
13999
+ var captureGeneration = ++generation;
14000
+ menu.setPointerCapture(pointer);
14001
+ window.addEventListener('pointermove', onPointerMove, true);
14002
+ window.addEventListener('pointerup', onPointerUp, true);
14003
+ window.addEventListener('pointercancel', onPointerCancel, true);
14004
+ if (float && float.isConnected) {
14005
+ if (float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
14006
+ float._liquidLandingTimer = 0;
14007
+ float.classList.remove('liquid-selection-landing');
14008
+ float.classList.add('visible');
14009
+ position(target);
14010
+ } else {
14011
+ var selected = menu.querySelector('button.model-select-menu-option.selected, button.newmark-select-option.selected');
14012
+ source = selected || pressed;
14013
+ source.classList.add('liquid-selection-source');
14014
+ float = document.createElement('div');
14015
+ float.className = 'liquid-selection-float liquid-selection-primed';
14016
+ mountLiquidViewportFloat(float);
14017
+ position(source);
14018
+ showLiquidViewportFloat(float);
14019
+ position(source);
14020
+ float.getBoundingClientRect();
14021
+ requestAnimationFrame(function() {
14022
+ if (!float) return;
14023
+ float.classList.remove('liquid-selection-primed');
14024
+ float.classList.add('visible');
14025
+ requestAnimationFrame(function() { if (float) position(target); });
14026
+ });
14027
+ }
14028
+ captureLiquidBackdropFrame().then(function(backdropFrame) {
14029
+ if (generation === captureGeneration && float && float.isConnected) attachKyantLiquidRenderer(float, backdropFrame);
14030
+ }).catch(function() {});
14031
+ });
14032
+ menu.addEventListener('click', function(event) {
14033
+ if (!event.isTrusted || !float || !float.isConnected) return;
14034
+ event.preventDefault();
14035
+ event.stopImmediatePropagation();
14036
+ }, true);
14037
+ }
14038
+
14039
+ // The same single floating capsule is used by the left workspace rail and
14040
+ // right sidebar tabs. Delegation keeps it valid while either rail rerenders.
14041
+ function wireLiquidRailInteractions(container, selector) {
14042
+ if (!container || container.dataset.liquidRailInteractions === 'true') return;
14043
+ container.dataset.liquidRailInteractions = 'true';
14044
+ var pointer = null;
14045
+ var source = null;
14046
+ var target = null;
14047
+ var float = null;
14048
+ var generation = 0;
14049
+ var pressX = 0;
14050
+ var pressY = 0;
14051
+ var gestureMoved = false;
14052
+ function itemAt(event) {
14053
+ var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
14054
+ var node = hit && hit.closest ? hit.closest(selector) : null;
14055
+ return node && container.contains(node) && !node.disabled ? node : null;
14056
+ }
14057
+ function position(item) {
14058
+ if (!item || !float) return;
14059
+ if (float.classList.contains('liquid-selection-viewport')) {
14060
+ var rect = item.getBoundingClientRect();
14061
+ var edge = liquidInteractionEdgePx();
14062
+ keepLiquidFloatExpansionInsideViewport(float, rect);
14063
+ float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
14064
+ float.style.left = (rect.left - edge) + 'px';
14065
+ float.style.top = (rect.top - edge) + 'px';
14066
+ float.style.width = (rect.width + edge * 2) + 'px';
14067
+ float.style.height = (rect.height + edge * 2) + 'px';
14068
+ if (float._renderKyantGlass) float._renderKyantGlass({
14069
+ left: rect.left - edge, top: rect.top - edge,
14070
+ width: rect.width + edge * 2, height: rect.height + edge * 2
14071
+ }, true);
14072
+ return;
14073
+ }
14074
+ var edge = liquidInteractionEdgePx();
14075
+ float.style.left = (item.offsetLeft - edge) + 'px';
14076
+ float.style.top = (item.offsetTop - edge) + 'px';
14077
+ float.style.width = (item.offsetWidth + edge * 2) + 'px';
14078
+ float.style.height = (item.offsetHeight + edge * 2) + 'px';
14079
+ }
14080
+ function positionAlongPointer(event) {
14081
+ if (!float || !event) return;
14082
+ var items = Array.from(container.querySelectorAll(selector))
14083
+ .filter(function(item) { return !item.disabled && item.getClientRects().length; });
14084
+ if (!items.length) return;
14085
+ var rects = items.map(function(item) { return item.getBoundingClientRect(); });
14086
+ var centersX = rects.map(function(rect) { return rect.left + rect.width / 2; });
14087
+ var centersY = rects.map(function(rect) { return rect.top + rect.height / 2; });
14088
+ var horizontal = Math.max.apply(null, centersX) - Math.min.apply(null, centersX) >
14089
+ Math.max.apply(null, centersY) - Math.min.apply(null, centersY);
14090
+ var pointerAxis = horizontal ? event.clientX : event.clientY;
14091
+ var centers = horizontal ? centersX : centersY;
14092
+ var nearestIndex = 0;
14093
+ var nearestDistance = Infinity;
14094
+ centers.forEach(function(center, index) {
14095
+ var distance = Math.abs(pointerAxis - center);
14096
+ if (distance < nearestDistance) { nearestIndex = index; nearestDistance = distance; }
14097
+ });
14098
+ target = items[nearestIndex];
14099
+ var rect = rects[nearestIndex];
14100
+ var minCenter = Math.min.apply(null, centers);
14101
+ var maxCenter = Math.max.apply(null, centers);
14102
+ var projectedCenter = Math.max(minCenter, Math.min(maxCenter, pointerAxis));
14103
+ var edge = liquidInteractionEdgePx();
14104
+ var left = horizontal ? projectedCenter - rect.width / 2 - edge : rect.left - edge;
14105
+ var top = horizontal ? rect.top - edge : projectedCenter - rect.height / 2 - edge;
14106
+ float.style.left = left + 'px';
14107
+ float.style.top = top + 'px';
14108
+ float.style.width = (rect.width + edge * 2) + 'px';
14109
+ float.style.height = (rect.height + edge * 2) + 'px';
14110
+ }
14111
+ function detachWindowTracking() {
14112
+ window.removeEventListener('pointermove', onPointerMove, true);
14113
+ window.removeEventListener('pointerup', onPointerUp, true);
14114
+ window.removeEventListener('pointercancel', onPointerCancel, true);
14115
+ }
14116
+ function clear() {
14117
+ detachWindowTracking();
14118
+ if (source) source.classList.remove('liquid-selection-source');
14119
+ stopLiquidMotionTracking(float);
14120
+ if (float && float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
14121
+ if (float && float._releaseKyantGlass) float._releaseKyantGlass();
14122
+ if (float && float.parentNode) float.parentNode.removeChild(float);
14123
+ generation += 1;
14124
+ pointer = source = target = float = null;
14125
+ window.setLiquidSidebarGestureLock('rail:' + (container.id || selector), false);
14126
+ }
14127
+ function onPointerMove(event) {
14128
+ if (pointer === null || event.pointerId !== pointer) return;
14129
+ if (!gestureMoved && Math.hypot(event.clientX - pressX, event.clientY - pressY) >= 4) {
14130
+ gestureMoved = true;
14131
+ }
14132
+ event.preventDefault();
14133
+ positionAlongPointer(event);
14134
+ }
14135
+ function onPointerUp(event) {
14136
+ if (pointer === null || event.pointerId !== pointer) return;
14137
+ var commit = target;
14138
+ detachWindowTracking();
14139
+ try { container.releasePointerCapture(pointer); } catch (_) {}
14140
+ pointer = null;
14141
+ if (commit) {
14142
+ position(commit);
14143
+ }
14144
+ waitForLiquidSelectionArrival(float, function() {
14145
+ landLiquidSelectionFloat(float, clear, function() {
14146
+ if (!commit || !commit.isConnected) return;
14147
+ if (container.id === 'left-tool-surface' || container.id === 'left-thumb') {
14148
+ container.querySelectorAll('.liquid-tool-active').forEach(function(item) { item.classList.remove('liquid-tool-active'); });
14149
+ commit.classList.add('liquid-tool-active');
14150
+ }
14151
+ commit.click();
14152
+ });
14153
+ });
14154
+ }
14155
+ function onPointerCancel(event) {
14156
+ if (pointer === null || event.pointerId === pointer) clear();
14157
+ }
14158
+ container.addEventListener('pointerdown', function(event) {
14159
+ if (window.liquidSidebarGesturesLocked() && !(float && float.isConnected)) return;
14160
+ var pressed = itemAt(event);
14161
+ if (!pressed || event.button !== 0) return;
14162
+ event.preventDefault();
14163
+ target = pressed;
14164
+ pointer = event.pointerId;
14165
+ pressX = event.clientX;
14166
+ pressY = event.clientY;
14167
+ gestureMoved = false;
14168
+ var captureGeneration = ++generation;
14169
+ window.setLiquidSidebarGestureLock('rail:' + (container.id || selector), true);
14170
+ container.setPointerCapture(pointer);
14171
+ window.addEventListener('pointermove', onPointerMove, true);
14172
+ window.addEventListener('pointerup', onPointerUp, true);
14173
+ window.addEventListener('pointercancel', onPointerCancel, true);
14174
+ if (float && float.isConnected) {
14175
+ if (float._liquidLandingTimer) clearTimeout(float._liquidLandingTimer);
14176
+ float._liquidLandingTimer = 0;
14177
+ float.classList.remove('liquid-selection-landing');
14178
+ float.classList.add('visible');
14179
+ position(target);
14180
+ } else {
14181
+ source = container.querySelector('.liquid-tool-active') || container.querySelector(selector + '.active') || pressed;
14182
+ source.classList.add('liquid-selection-source');
14183
+ float = document.createElement('div');
14184
+ float.className = 'liquid-selection-float liquid-selection-primed';
14185
+ mountLiquidViewportFloat(float);
14186
+ position(source);
14187
+ showLiquidViewportFloat(float);
14188
+ position(source);
14189
+ float.getBoundingClientRect();
14190
+ requestAnimationFrame(function() {
14191
+ if (!float) return;
14192
+ float.classList.remove('liquid-selection-primed');
14193
+ float.classList.add('visible');
14194
+ requestAnimationFrame(function() { if (float) position(target); });
14195
+ });
14196
+ }
14197
+ captureLiquidBackdropFrame().then(function(backdropFrame) {
14198
+ if (generation === captureGeneration && float && float.isConnected) attachKyantLiquidRenderer(float, backdropFrame);
14199
+ }).catch(function() {});
14200
+ });
14201
+ container.addEventListener('click', function(event) {
14202
+ if (!event.isTrusted || !float || !float.isConnected) return;
14203
+ event.preventDefault();
14204
+ event.stopImmediatePropagation();
14205
+ }, true);
14206
+ }
14207
+
14208
+ requestAnimationFrame(function() {
14209
+ wireLiquidRailInteractions(document.getElementById('mode-toggle'), '.mode-toggle-btn[data-mode]');
14210
+ wireLiquidRailInteractions(document.getElementById('left-tool-surface'), '.left-nav-icon');
14211
+ wireLiquidRailInteractions(document.getElementById('left-thumb'), '.lt-item');
14212
+ wireLiquidRailInteractions(document.getElementById('left-ws-list'), '.left-ws-item');
14213
+ wireLiquidRailInteractions(document.getElementById('right-tabs'), '.tab-btn[data-tab]');
14214
+ });
14215
+
14216
+ // Checkbox switches use one pointer-captured glass thumb that follows the
14217
+ // pointer continuously, then commits to the nearest binary stop on release.
14218
+ document.addEventListener('pointerdown', function(event) {
14219
+ var input = event.target && event.target.closest ? event.target.closest('input[type="checkbox"]') : null;
14220
+ if (!input || input.disabled || event.button !== 0) return;
14221
+ event.preventDefault();
14222
+ var rect = input.getBoundingClientRect();
14223
+ var pointer = event.pointerId;
14224
+ var fraction = input.checked ? 1 : 0;
14225
+ var startX = event.clientX;
14226
+ var startY = event.clientY;
14227
+ var dragging = false;
14228
+ var canceledToScroll = false;
14229
+ var dragThreshold = event.pointerType === 'mouse' ? 4 : 8;
14230
+ input.dataset.liquidSuppressClick = 'true';
14231
+ clearTimeout(input._liquidSuppressClickTimer);
14232
+ input._liquidSuppressClickTimer = setTimeout(function() {
14233
+ delete input.dataset.liquidSuppressClick;
14234
+ delete input._liquidSuppressClickTimer;
14235
+ }, 700);
14236
+ function update(clientX) {
14237
+ fraction = ((clientX - rect.left - 12) / Math.max(1, rect.width - 24));
14238
+ fraction = Math.max(0, Math.min(1, fraction));
14239
+ input.style.setProperty('--liquid-switch-x', (18 * fraction) + 'px');
14240
+ input.style.setProperty('--liquid-switch-progress', (100 * fraction) + '%');
14241
+ }
14242
+ function finish(commit) {
14243
+ input.removeEventListener('pointermove', move);
14244
+ input.removeEventListener('pointerup', up);
14245
+ input.removeEventListener('pointercancel', cancel);
14246
+ input.classList.remove('liquid-switch-dragging');
14247
+ input.style.removeProperty('--liquid-switch-x');
14248
+ input.style.removeProperty('--liquid-switch-progress');
14249
+ try { input.releasePointerCapture(pointer); } catch (_) {}
14250
+ if (!commit || canceledToScroll) return;
14251
+ var next = dragging ? fraction >= 0.5 : !input.checked;
14252
+ var changed = input.checked !== next;
14253
+ input.checked = next;
14254
+ if (changed) {
14255
+ input.dispatchEvent(new Event('input', { bubbles: true }));
14256
+ input.dispatchEvent(new Event('change', { bubbles: true }));
14257
+ }
14258
+ }
14259
+ function move(moveEvent) {
14260
+ if (moveEvent.pointerId !== pointer) return;
14261
+ if (!dragging) {
14262
+ var deltaX = moveEvent.clientX - startX;
14263
+ var deltaY = moveEvent.clientY - startY;
14264
+ if (Math.hypot(deltaX, deltaY) < dragThreshold) return;
14265
+ if (Math.abs(deltaY) > Math.abs(deltaX)) {
14266
+ canceledToScroll = true;
14267
+ return;
14268
+ }
14269
+ dragging = true;
14270
+ }
14271
+ moveEvent.preventDefault();
14272
+ update(moveEvent.clientX);
14273
+ }
14274
+ function up(upEvent) {
14275
+ if (upEvent.pointerId === pointer) finish(true);
14276
+ }
14277
+ function cancel(cancelEvent) {
14278
+ if (cancelEvent.pointerId === pointer) finish(false);
14279
+ }
14280
+ input.classList.add('liquid-switch-dragging');
14281
+ input.setPointerCapture(pointer);
14282
+ input.addEventListener('pointermove', move);
14283
+ input.addEventListener('pointerup', up);
14284
+ input.addEventListener('pointercancel', cancel);
14285
+ }, true);
14286
+ document.addEventListener('click', function(event) {
14287
+ var input = event.target && event.target.closest ? event.target.closest('input[type="checkbox"]') : null;
14288
+ if (!input || input.dataset.liquidSuppressClick !== 'true') return;
14289
+ clearTimeout(input._liquidSuppressClickTimer);
14290
+ delete input.dataset.liquidSuppressClick;
14291
+ delete input._liquidSuppressClickTimer;
14292
+ event.preventDefault();
14293
+ event.stopImmediatePropagation();
14294
+ }, true);
14295
+
14296
+ // Glass-strength range mirrors the binary liquid switch: its existing color
14297
+ // thumb lifts into one glass capsule, follows the pointer continuously, then
14298
+ // falls back to the color thumb and persists the released value.
14299
+ document.addEventListener('pointerdown', function(event) {
14300
+ var input = event.target && event.target.closest ? event.target.closest('#settings-glass-opacity') : null;
14301
+ if (!input || input.disabled || event.button !== 0) return;
14302
+ event.preventDefault();
14303
+ var rect = input.getBoundingClientRect();
14304
+ var pointer = event.pointerId;
14305
+ var minimum = Number(input.min || 0);
14306
+ var maximum = Number(input.max || 100);
14307
+ var step = Math.max(0.0001, Number(input.step || 1));
14308
+ var initialValue = input.value;
14309
+ function update(clientX) {
14310
+ var fraction = Math.max(0, Math.min(1, (clientX - rect.left) / Math.max(1, rect.width)));
14311
+ var raw = minimum + fraction * (maximum - minimum);
14312
+ var next = Math.min(maximum, Math.max(minimum, Math.round((raw - minimum) / step) * step + minimum));
14313
+ input.value = String(next);
14314
+ input.style.setProperty('--liquid-range-progress', (fraction * 100) + '%');
14315
+ input.dispatchEvent(new Event('input', { bubbles: true }));
14316
+ }
14317
+ function finish(commit) {
14318
+ input.removeEventListener('pointermove', move);
14319
+ input.removeEventListener('pointerup', up);
14320
+ input.removeEventListener('pointercancel', cancel);
14321
+ input.classList.remove('liquid-range-dragging');
14322
+ try { input.releasePointerCapture(pointer); } catch (_) {}
14323
+ if (!commit) {
14324
+ input.value = initialValue;
14325
+ input.dispatchEvent(new Event('input', { bubbles: true }));
14326
+ return;
14327
+ }
14328
+ input.dataset.liquidSuppressRangeClick = 'true';
14329
+ input.dispatchEvent(new Event('change', { bubbles: true }));
14330
+ }
14331
+ function move(moveEvent) { if (moveEvent.pointerId === pointer) { moveEvent.preventDefault(); update(moveEvent.clientX); } }
14332
+ function up(upEvent) { if (upEvent.pointerId === pointer) finish(true); }
14333
+ function cancel(cancelEvent) { if (cancelEvent.pointerId === pointer) finish(false); }
14334
+ input.classList.add('liquid-range-dragging');
14335
+ update(event.clientX);
14336
+ input.setPointerCapture(pointer);
14337
+ input.addEventListener('pointermove', move);
14338
+ input.addEventListener('pointerup', up);
14339
+ input.addEventListener('pointercancel', cancel);
14340
+ }, true);
14341
+ document.addEventListener('click', function(event) {
14342
+ var input = event.target && event.target.closest ? event.target.closest('#settings-glass-opacity') : null;
14343
+ if (!input || input.dataset.liquidSuppressRangeClick !== 'true') return;
14344
+ delete input.dataset.liquidSuppressRangeClick;
14345
+ event.preventDefault();
14346
+ event.stopImmediatePropagation();
14347
+ }, true);
14348
+
12698
14349
  window.syncModelSelectSurface = function() {
12699
14350
  var sel = els['model-select'] || document.getElementById('model-select');
12700
14351
  var label = document.getElementById('model-select-label');
@@ -12716,6 +14367,7 @@ window.syncModelSelectSurface = function() {
12716
14367
  }
12717
14368
  });
12718
14369
  menu.innerHTML = html;
14370
+ wireLiquidMenuInteractions(menu);
12719
14371
  };
12720
14372
 
12721
14373
  document.addEventListener('pointerdown', function(event) {
@@ -12727,6 +14379,7 @@ window.closeNewmarkSelect = function(shell, options) {
12727
14379
  shell = shell || document.querySelector('.newmark-select-shell.open');
12728
14380
  if (!shell) return;
12729
14381
  shell.classList.remove('open');
14382
+ window.setLiquidSidebarGestureLock('popup:select:' + (shell.dataset.selectId || 'anonymous'), false);
12730
14383
  var menu = shell.querySelector('.newmark-select-menu');
12731
14384
  if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
12732
14385
  var button = shell.querySelector('.newmark-select-button');
@@ -12810,6 +14463,7 @@ window.syncNewmarkSelect = function(select) {
12810
14463
  }
12811
14464
  });
12812
14465
  menu.innerHTML = html;
14466
+ wireLiquidMenuInteractions(menu);
12813
14467
  };
12814
14468
 
12815
14469
  window.enhanceNewmarkSelect = function(select) {
@@ -12850,7 +14504,7 @@ window.enhanceNewmarkSelect = function(select) {
12850
14504
  button.setAttribute('aria-label', String(accessibleLabel).trim());
12851
14505
  button.innerHTML = '<span class="newmark-select-button-label"></span>' + iconSvg('chevron-down', '', 'tiny');
12852
14506
  var menu = document.createElement('div');
12853
- menu.className = 'model-select-menu newmark-select-menu';
14507
+ menu.className = 'model-select-menu newmark-select-menu liquid-glass liquid-glass-carrier';
12854
14508
  menu.setAttribute('role', 'listbox');
12855
14509
  menu.setAttribute('popover', 'manual');
12856
14510
  select.parentNode.insertBefore(shell, select);
@@ -12871,6 +14525,7 @@ window.enhanceNewmarkSelect = function(select) {
12871
14525
  }
12872
14526
  window.syncNewmarkSelect(select);
12873
14527
  shell.classList.add('open');
14528
+ window.setLiquidSidebarGestureLock('popup:select:' + (shell.dataset.selectId || 'anonymous'), true);
12874
14529
  button.setAttribute('aria-expanded', 'true');
12875
14530
  if (opening) {
12876
14531
  if (typeof menu.showPopover === 'function' && !menu.matches(':popover-open')) menu.showPopover();
@@ -13169,6 +14824,47 @@ function backendQueueForTarget(target) {
13169
14824
  return normalizeBackendQueue(state.backendQueuesByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)]);
13170
14825
  }
13171
14826
 
14827
+ function queueItemsForTarget(target) {
14828
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
14829
+ if (!owner) return [];
14830
+ if (!state.queueItemsByTarget) state.queueItemsByTarget = {};
14831
+ return state.queueItemsByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)] || [];
14832
+ }
14833
+
14834
+ function setQueueItemsForTarget(items, target) {
14835
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
14836
+ if (!owner) return;
14837
+ if (!state.queueItemsByTarget) state.queueItemsByTarget = {};
14838
+ state.queueItemsByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)] = (Array.isArray(items) ? items : []).map(function(item) {
14839
+ return {
14840
+ id: String(item && item.id || ''),
14841
+ text: String(item && item.text || ''),
14842
+ queueMode: String(item && item.queueMode || 'followUp'),
14843
+ requestedMode: String(item && item.requestedMode || 'build'),
14844
+ goalObjective: String(item && item.goalObjective || ''),
14845
+ runId: String(item && item.runId || ''),
14846
+ createdAt: String(item && item.createdAt || ''),
14847
+ };
14848
+ });
14849
+ }
14850
+
14851
+ function queueItemIdForText(text, target) {
14852
+ var items = queueItemsForTarget(target);
14853
+ var normalized = String(text || '');
14854
+ for (var i = 0; i < items.length; i++) {
14855
+ if (String(items[i].text || '') === normalized) return items[i].id;
14856
+ }
14857
+ return '';
14858
+ }
14859
+
14860
+ function syncQueueItemsFromSnapshot(snapshot, target) {
14861
+ if (snapshot && Array.isArray(snapshot.queueItems)) setQueueItemsForTarget(snapshot.queueItems, target);
14862
+ }
14863
+
14864
+ window.queueAction = function(action, input) {
14865
+ if (!api.queueAction) return Promise.resolve({ ok: false, error: 'queueAction unavailable' });
14866
+ return api.queueAction(action, input);
14867
+ };
13172
14868
  function setBackendQueueForTarget(queue, target) {
13173
14869
  var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
13174
14870
  var normalized = normalizeBackendQueue(queue);
@@ -13248,19 +14944,22 @@ window.renderQueuePanel = function() {
13248
14944
  if (queueIndex === state._editingQueueIndex) continue;
13249
14945
  var request = state.nextQueueRequests[queueIndex];
13250
14946
  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 ? '' :
14947
+ // dev-0.5.5: a backend-managed item is editable/deletable when the kernel
14948
+ // exposed its stable queueItem id (mobile-originated queue entries now
14949
+ // carry ids through queueItems). Items without an id stay locked.
14950
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[queueIndex] || '', currentConversationTarget()) : '';
14951
+ var operable = !backendManaged || !!queueItemId;
14952
+ var dragAttributes = operable
14953
+ ? ' draggable="true" ondragstart="window.startQueueDrag(event,' + queueIndex + ')" ondragover="window.overQueueDrag(event,' + queueIndex + ')" ondrop="window.dropQueueDrag(event,' + queueIndex + ')" ondragend="window.endQueueDrag(event)"'
14954
+ : ' draggable="false"';
14955
+ var dragHandle = operable
14956
+ ? '<button class="queue-drag" title="' + escAttr(t('queue.title')) + '">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</button>'
14957
+ : '<span class="queue-drag" aria-hidden="true">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</span>';
14958
+ var inputAttributes = ' readonly aria-readonly="true"';
14959
+ var actions = operable ?
13261
14960
  '<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
14961
  '<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>';
14962
+ '<button class="stack-icon-btn danger" onclick="window.deleteQueueItem(' + queueIndex + ')" title="' + escAttr(t('common.delete')) + '">' + iconSvg('trash', t('common.delete'), 'tiny') + '</button>' : '';
13264
14963
  html += '<div class="queue-item"' + dragAttributes + '>' +
13265
14964
  dragHandle +
13266
14965
  '<input class="queue-edit" value="' + escAttr(state.nextQueue[queueIndex] || '') + '"' + inputAttributes + '>' +
@@ -13293,9 +14992,11 @@ window.editQueueItem = function(idx, value) {
13293
14992
 
13294
14993
  window.focusQueueItem = function(idx) {
13295
14994
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13296
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) return;
14995
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
14996
+ var backendManaged = queuedRequestIsBackendManaged(request);
14997
+ if (backendManaged && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget())) return;
13297
14998
  state._editingQueueIndex = idx;
13298
- state.pendingInputEdit = { kind: 'queue', index: idx, request: request, target: currentConversationTarget() };
14999
+ state.pendingInputEdit = { kind: 'queue', index: idx, request: request, target: currentConversationTarget(), originalText: String(state.nextQueue[idx] || '') };
13299
15000
  els.prompt.value = String(state.nextQueue[idx] || '');
13300
15001
  state.promptAttachments = (request.images || []).map(function(image, imageIndex) {
13301
15002
  return { id: 'queue-edit-' + imageIndex + '-' + Date.now(), name: image.name || 'queued image', type: image.type || 'image/png', dataUrl: image.dataUrl };
@@ -13309,13 +15010,23 @@ window.focusQueueItem = function(idx) {
13309
15010
  window.deleteQueueItem = function(idx) {
13310
15011
  if (!state.nextQueue) return;
13311
15012
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13312
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15013
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15014
+ var backendManaged = queuedRequestIsBackendManaged(request);
15015
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()) : '';
15016
+ if (backendManaged && !queueItemId) return;
13313
15017
  if (!state.queueHiddenItems) state.queueHiddenItems = {};
13314
15018
  if (state.nextQueue[idx]) state.queueHiddenItems[queueHiddenItemKey(state.nextQueue[idx], currentConversationTarget())] = true;
13315
15019
  state.nextQueue.splice(idx, 1);
13316
15020
  if (state.nextQueueRequests) state.nextQueueRequests.splice(idx, 1);
13317
15021
  refreshNextPromptForTarget(currentConversationTarget());
13318
15022
  window.renderInputStack();
15023
+ if (backendManaged) {
15024
+ window.queueAction('queue_delete', { id: queueItemId }).then(function(result) {
15025
+ if (!result || result.ok === false) {
15026
+ showUiNotice(result && result.error ? result.error : (currentLang() === 'zh' ? '删除队列项失败' : 'Failed to delete queue item'), 'error', 'queue-delete-' + queueItemId);
15027
+ }
15028
+ });
15029
+ }
13319
15030
  };
13320
15031
 
13321
15032
  function restoreQueueItemAfterGuideFailure(idx, text, request, target, reuseClientMessageId) {
@@ -13332,7 +15043,10 @@ function restoreQueueItemAfterGuideFailure(idx, text, request, target, reuseClie
13332
15043
  window.guideQueueItem = function(idx) {
13333
15044
  if (!state.nextQueue || !state.nextQueue[idx]) return;
13334
15045
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13335
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15046
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15047
+ var backendManaged = queuedRequestIsBackendManaged(request);
15048
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()) : '';
15049
+ if (backendManaged && !queueItemId) return;
13336
15050
  var target = normalizeQueuedConversationTarget(request.target) || currentConversationTarget();
13337
15051
  var deliverDuringRun = isCurrentConversationRunning();
13338
15052
  var next = state.nextQueue.splice(idx, 1)[0];
@@ -13341,6 +15055,20 @@ window.guideQueueItem = function(idx) {
13341
15055
  if (next) state.queueHiddenItems[queueHiddenItemKey(next, target)] = true;
13342
15056
  refreshNextPromptForTarget(currentConversationTarget());
13343
15057
  window.renderInputStack();
15058
+ if (backendManaged) {
15059
+ // The kernel owns this entry; guide it there so the mobile side stays
15060
+ // in sync. On failure restore the local row.
15061
+ return window.queueAction('queue_guide', { id: queueItemId }).then(function(outcome) {
15062
+ if (!outcome || outcome.ok === false) {
15063
+ restoreQueueItemAfterGuideFailure(idx, next, nextRequest, target, true);
15064
+ showUiNotice(outcome && outcome.error ? outcome.error : (currentLang() === 'zh' ? 'Guide 投递失败' : 'Guide delivery failed'), 'error', 'queue-guide-' + queueItemId);
15065
+ }
15066
+ return outcome || { ok: false };
15067
+ }, function(error) {
15068
+ restoreQueueItemAfterGuideFailure(idx, next, nextRequest, target, true);
15069
+ return { ok: false, error: error && error.message ? error.message : String(error) };
15070
+ });
15071
+ }
13344
15072
  if (next) {
13345
15073
  var delivery = window.sendMessage('guide', next, {
13346
15074
  fromQueue: true,
@@ -13365,7 +15093,11 @@ window.guideQueueItem = function(idx) {
13365
15093
 
13366
15094
  window.startQueueDrag = function(event, idx) {
13367
15095
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
13368
- if (queuedRequestIsBackendManaged(request) || !queuedRequestMatchesTarget(request, currentConversationTarget())) {
15096
+ if (!queuedRequestMatchesTarget(request, currentConversationTarget())) {
15097
+ state.queueDragIndex = -1;
15098
+ return;
15099
+ }
15100
+ if (queuedRequestIsBackendManaged(request) && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget())) {
13369
15101
  state.queueDragIndex = -1;
13370
15102
  return;
13371
15103
  }
@@ -13386,9 +15118,12 @@ window.dropQueueDrag = function(event, idx) {
13386
15118
  if (!state.nextQueue || from < 0 || from === idx || from >= state.nextQueue.length) return;
13387
15119
  var sourceRequest = state.nextQueueRequests && state.nextQueueRequests[from];
13388
15120
  var destinationRequest = state.nextQueueRequests && state.nextQueueRequests[idx];
13389
- if (queuedRequestIsBackendManaged(sourceRequest) || queuedRequestIsBackendManaged(destinationRequest)
13390
- || !queuedRequestMatchesTarget(sourceRequest, currentConversationTarget())
15121
+ if (!queuedRequestMatchesTarget(sourceRequest, currentConversationTarget())
13391
15122
  || !queuedRequestMatchesTarget(destinationRequest, currentConversationTarget())) return;
15123
+ var sourceBackend = queuedRequestIsBackendManaged(sourceRequest);
15124
+ var destinationBackend = queuedRequestIsBackendManaged(destinationRequest);
15125
+ if ((sourceBackend && !queueItemIdForText(state.nextQueue[from] || '', currentConversationTarget()))
15126
+ || (destinationBackend && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()))) return;
13392
15127
  var item = state.nextQueue.splice(from, 1)[0];
13393
15128
  var requestItem = state.nextQueueRequests ? state.nextQueueRequests.splice(from, 1)[0] : null;
13394
15129
  state.nextQueue.splice(idx, 0, item);
@@ -13396,6 +15131,17 @@ window.dropQueueDrag = function(event, idx) {
13396
15131
  state.queueDragIndex = -1;
13397
15132
  refreshNextPromptForTarget(currentConversationTarget());
13398
15133
  window.renderInputStack();
15134
+ if (sourceBackend || destinationBackend) {
15135
+ // The kernel owns at least one affected entry: forward the complete
15136
+ // ordered id list so mobile and desktop stay in sync.
15137
+ var visibleIndexes = queueIndexesForTarget(currentConversationTarget());
15138
+ var orderedIds = visibleIndexes.map(function(vi) {
15139
+ return queueItemIdForText(state.nextQueue[vi] || '', currentConversationTarget());
15140
+ }).filter(function(id) { return !!id; });
15141
+ if (orderedIds.length) {
15142
+ window.queueAction('queue_reorder', { orderedIds: orderedIds });
15143
+ }
15144
+ }
13399
15145
  };
13400
15146
 
13401
15147
  window.endQueueDrag = function(event) {
@@ -13445,7 +15191,24 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13445
15191
  els.prompt.dispatchEvent(new Event('input', { bubbles: true }));
13446
15192
  if (pendingEdit.kind === 'queue') {
13447
15193
  var editIndex = Number(pendingEdit.index);
15194
+ var editRequest = pendingEdit.request || (state.nextQueueRequests && state.nextQueueRequests[editIndex]);
15195
+ var editBackendManaged = queuedRequestIsBackendManaged(editRequest);
15196
+ var editQueueItemId = editBackendManaged ? queueItemIdForText(String(state.nextQueue[editIndex] || pendingEdit.originalText || ''), pendingEdit.target) : '';
15197
+ if (editBackendManaged && !editQueueItemId) {
15198
+ showUiNotice(currentLang() === 'zh' ? '无法编辑该队列项:缺少后端 id' : 'Cannot edit queue item: backend id missing', 'error', 'queue-edit-' + editIndex);
15199
+ return { ok: false, error: 'missing backend queue id' };
15200
+ }
13448
15201
  if (effectiveInputMode === 'next') {
15202
+ if (editBackendManaged) {
15203
+ // The kernel owns this entry: forward the edit and let the next
15204
+ // snapshot reconcile the local row.
15205
+ window.queueAction('queue_update', { id: editQueueItemId, text: editedValue, requestedMode: String(editRequest && editRequest.requestedMode || 'build'), goalObjective: String(editRequest && editRequest.goalObjective || ''), createdAt: String(editRequest && editRequest.createdAt || '') });
15206
+ state.nextQueue[editIndex] = editedValue;
15207
+ if (state.nextQueueRequests[editIndex]) state.nextQueueRequests[editIndex].text = editedValue;
15208
+ refreshNextPromptForTarget(pendingEdit.target);
15209
+ window.renderInputStack();
15210
+ return { ok: true, queued: true };
15211
+ }
13449
15212
  state.nextQueue[editIndex] = editedValue;
13450
15213
  if (state.nextQueueRequests[editIndex]) {
13451
15214
  state.nextQueueRequests[editIndex].text = editedValue;
@@ -13808,7 +15571,11 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13808
15571
  }));
13809
15572
  var lockedUi = conversationWorkUiState(lockedConversationId, lockedTarget.workspaceId);
13810
15573
  if (stillActive && r && Array.isArray(r.diffs) && r.diffs.length) {
13811
- lockedUi.pendingWorkReview = r.diffs;
15574
+ var reviewRecord = {
15575
+ diffs: r.diffs,
15576
+ runId: String(r.runId || localRunId || '')
15577
+ };
15578
+ upsertWorkReviewRecord(lockedUi, reviewRecord.diffs, reviewRecord.runId, [localRunId]);
13812
15579
  }
13813
15580
  if (stillActive && !responseOwnedByRun) {
13814
15581
  responseMsg = lockedUi.activeWorkflowMsg;
@@ -13837,9 +15604,13 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13837
15604
  hydrateConversationBranchState(r);
13838
15605
  renderChatMessages(r.chatMessages);
13839
15606
  }
13840
- else if (stillActive) {
15607
+ // Re-render every retained completed-run review after a transcript rebuild.
15608
+ // addWorkReview dedupes by run identity and normalized file totals.
15609
+ if (stillActive) {
13841
15610
  var completedReviewUi = conversationWorkUiState(lockedConversationId);
13842
- if (completedReviewUi.pendingWorkReview) addWorkReview(completedReviewUi.pendingWorkReview);
15611
+ (completedReviewUi.workReviews || []).forEach(function(item) {
15612
+ addWorkReview(item.diffs || item, item.runId || '', [localRunId]);
15613
+ });
13843
15614
  }
13844
15615
  if (r && r.conversations) {
13845
15616
  applyBackendConversations(r.conversations || [], stillActive ? lockedConversationId : activeConversationId(), lockedTarget.workspaceId);
@@ -13860,6 +15631,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13860
15631
  if (r && r.queued) {
13861
15632
  var responseQueue = setBackendQueueForTarget(r.queued, lockedTarget);
13862
15633
  window.syncNextQueueFromBackend(responseQueue, lockedTarget);
15634
+ syncQueueItemsFromSnapshot(r, lockedTarget);
13863
15635
  if (stillActive) state.backendQueue = responseQueue;
13864
15636
  }
13865
15637
  if (r && r.options) {
@@ -13905,6 +15677,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13905
15677
  if (s && s.queued) {
13906
15678
  var refreshedQueue = setBackendQueueForTarget(s.queued, lockedTarget);
13907
15679
  window.syncNextQueueFromBackend(refreshedQueue, lockedTarget);
15680
+ syncQueueItemsFromSnapshot(s, lockedTarget);
13908
15681
  if (stillActiveAfterRefresh) state.backendQueue = refreshedQueue;
13909
15682
  }
13910
15683
  if (s && s.conversations) {
@@ -14349,6 +16122,7 @@ window.switchRightTab = function(tab) {
14349
16122
  var rightTabs = els['right-tabs'] || document.getElementById('right-tabs');
14350
16123
  var rightContent = els['right-content'] || document.getElementById('right-content');
14351
16124
  if (!rightTabs || !rightContent) return;
16125
+ wireLiquidRailInteractions(rightTabs, '.tab-btn[data-tab]');
14352
16126
  var btns = rightTabs.querySelectorAll('.tab-btn[data-tab]');
14353
16127
  for (var i = 0; i < btns.length; i++) {
14354
16128
  var selected = btns[i].getAttribute('data-tab') === tab;
@@ -14753,6 +16527,57 @@ var _terminalCounter = 0;
14753
16527
  var _terminalTakeoverTabs = {};
14754
16528
  var _terminalTakeoverRefreshToken = 0;
14755
16529
  var _terminalTakeoverDetached = {};
16530
+ var _terminalOutputLimit = 256 * 1024;
16531
+
16532
+ window.appendTerminalOutput = function(output, text, style) {
16533
+ if (!output || !text) return;
16534
+ var state = output._terminalRenderState;
16535
+ if (!state) state = output._terminalRenderState = { pending: [], frame: 0 };
16536
+ state.pending.push({ text: String(text), style: style || null });
16537
+ if (state.frame) return;
16538
+ state.frame = requestAnimationFrame(function() {
16539
+ state.frame = 0;
16540
+ if (!output.isConnected) { state.pending.length = 0; return; }
16541
+ var nearBottom = output.scrollHeight - output.scrollTop - output.clientHeight <= 32;
16542
+ var fragment = document.createDocumentFragment();
16543
+ for (var i = 0; i < state.pending.length; i++) {
16544
+ var item = state.pending[i];
16545
+ if (item.style) {
16546
+ var span = document.createElement('span');
16547
+ span.textContent = item.text;
16548
+ if (item.style.color) span.style.color = item.style.color;
16549
+ if (item.style.opacity) span.style.opacity = item.style.opacity;
16550
+ fragment.appendChild(span);
16551
+ } else {
16552
+ fragment.appendChild(document.createTextNode(item.text));
16553
+ }
16554
+ }
16555
+ state.pending.length = 0;
16556
+ output.appendChild(fragment);
16557
+ var overflow = output.textContent.length - _terminalOutputLimit;
16558
+ while (overflow > 0 && output.firstChild) {
16559
+ var first = output.firstChild;
16560
+ var length = first.textContent.length;
16561
+ if (length <= overflow) {
16562
+ output.removeChild(first);
16563
+ overflow -= length;
16564
+ } else {
16565
+ first.textContent = first.textContent.slice(overflow);
16566
+ overflow = 0;
16567
+ }
16568
+ }
16569
+ if (nearBottom) output.scrollTop = output.scrollHeight;
16570
+ });
16571
+ };
16572
+
16573
+ window.setTerminalOutput = function(output, text, style) {
16574
+ if (!output) return;
16575
+ var state = output._terminalRenderState;
16576
+ if (state && state.frame) cancelAnimationFrame(state.frame);
16577
+ output._terminalRenderState = { pending: [], frame: 0 };
16578
+ output.textContent = '';
16579
+ window.appendTerminalOutput(output, text, style);
16580
+ };
14756
16581
 
14757
16582
  window.portableTerminalWorkspacePath = function(input) {
14758
16583
  var raw = String(input || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
@@ -14835,14 +16660,14 @@ window.terminalSend = function() {
14835
16660
  if (takeoverSession) {
14836
16661
  input.value = '';
14837
16662
  if (!api.terminalTakeoverWrite) {
14838
- if (output) output.innerHTML += '\r\n<span style="color:#ff6666;">[' + esc(t('terminal.notConnected')) + ']</span>';
16663
+ if (output) window.appendTerminalOutput(output, '\r\n[' + t('terminal.notConnected') + ']', { color:'#ff6666' });
14839
16664
  return;
14840
16665
  }
14841
16666
  var takeoverConversation = pane.getAttribute('data-takeover-conversation') || activeConversationId();
14842
16667
  api.terminalTakeoverWrite(takeoverSession, cmd, takeoverConversation).then(function(result) {
14843
16668
  if (result && result.ok === false) throw new Error(result.error || 'Terminal write failed');
14844
16669
  }).catch(function(err) {
14845
- if (output) output.innerHTML += '\r\n<span style="color:#ff6666;">[' + esc(t('common.error')) + '] ' + esc(err.message) + '</span>';
16670
+ if (output) window.appendTerminalOutput(output, '\r\n[' + t('common.error') + '] ' + err.message, { color:'#ff6666' });
14846
16671
  });
14847
16672
  return;
14848
16673
  }
@@ -14861,12 +16686,12 @@ window.terminalSend = function() {
14861
16686
  input.disabled = false;
14862
16687
  var currentPane = window.getActiveTerminalPane();
14863
16688
  var currentOutput = (currentPane && currentPane.querySelector('.terminal-output')) || output;
14864
- if (currentOutput) currentOutput.innerHTML += '\r\n<span style="color:#ff6666;">[' + esc(t('common.error')) + '] ' + esc(err.message || String(err)) + '</span>';
16689
+ if (currentOutput) window.appendTerminalOutput(currentOutput, '\r\n[' + t('common.error') + '] ' + (err.message || String(err)), { color:'#ff6666' });
14865
16690
  });
14866
16691
  }
14867
16692
  input.value = '';
14868
16693
  api.terminalWrite(sessionId, cmd + '\r').catch(function(err) {
14869
- if (output) output.innerHTML += '\r\n<span style="color:#ff6666;">[' + esc(t('common.error')) + '] ' + esc(err.message) + '</span>';
16694
+ if (output) window.appendTerminalOutput(output, '\r\n[' + t('common.error') + '] ' + err.message, { color:'#ff6666' });
14870
16695
  });
14871
16696
  };
14872
16697
 
@@ -14880,7 +16705,7 @@ window.clearTerminal = function() {
14880
16705
  api.terminalKill(sessionId, state.terminalInterruptTimeoutMs || 0).catch(function(){});
14881
16706
  pane.setAttribute('data-session', '');
14882
16707
  }
14883
- if (output) output.innerHTML = '';
16708
+ if (output) window.setTerminalOutput(output, '');
14884
16709
  if (prompt) prompt.textContent = '>';
14885
16710
  };
14886
16711
 
@@ -14990,12 +16815,12 @@ window.addTerminalTab = function(shellId, options) {
14990
16815
  var terminalReadyPromise = terminalSpawnPromise.then(function(resp) {
14991
16816
  pane.setAttribute('data-session', resp.sessionId);
14992
16817
  var out = pane.querySelector('.terminal-output');
14993
- if (out) out.innerHTML = '<span style="color:var(--accent2);">' + esc(t('terminal.connected')) + ' (' + esc(shellId) + ')</span>\r\n';
16818
+ if (out) window.setTerminalOutput(out, t('terminal.connected') + ' (' + shellId + ')\r\n', { color:'var(--accent2)' });
14994
16819
  state._terminalActiveSession = resp.sessionId;
14995
16820
  return resp;
14996
16821
  }).catch(function(err) {
14997
16822
  var out = pane.querySelector('.terminal-output');
14998
- if (out) out.innerHTML = '<span style="color:#ff6666;">' + esc(t('terminal.failed')) + ': ' + esc(err.message) + '</span>';
16823
+ if (out) window.setTerminalOutput(out, t('terminal.failed') + ': ' + err.message, { color:'#ff6666' });
14999
16824
  if (options.required === true) throw err;
15000
16825
  return null;
15001
16826
  });
@@ -15110,13 +16935,12 @@ window.applyTerminalTakeoverEvent = function(payload) {
15110
16935
  var output = pane.querySelector('.terminal-output');
15111
16936
  if (!output) return;
15112
16937
  if (payload.type === 'started' && session.buffer) {
15113
- output.innerHTML = esc(session.buffer);
16938
+ window.setTerminalOutput(output, session.buffer);
15114
16939
  } else if (payload.data) {
15115
- output.innerHTML += esc(payload.data);
16940
+ window.appendTerminalOutput(output, payload.data);
15116
16941
  } else if (payload.type === 'stopped') {
15117
- output.innerHTML += '\r\n<span style="color:var(--text-dim);opacity:0.65;">[Agent takeover stopped]</span>';
16942
+ window.appendTerminalOutput(output, '\r\n[Agent takeover stopped]', { color:'var(--text-dim)', opacity:'0.65' });
15118
16943
  }
15119
- output.scrollTop = output.scrollHeight;
15120
16944
  if (!active) window.pruneTerminalTakeoverEndedPanes(session.id);
15121
16945
  };
15122
16946
 
@@ -15236,16 +17060,14 @@ if (api.onTerminalData) {
15236
17060
  .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '')
15237
17061
  .replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, '')
15238
17062
  .replace(/\r(?!\n)/g, '');
15239
- var text = clean.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
15240
- output.innerHTML += text;
15241
- output.scrollTop = output.scrollHeight;
17063
+ window.appendTerminalOutput(output, clean);
15242
17064
  });
15243
17065
  api.onTerminalExit(function(event, sessionId, code) {
15244
17066
  var pane = document.querySelector('.terminal-pane[data-session="' + sessionId + '"]');
15245
17067
  if (!pane) return;
15246
17068
  pane.setAttribute('data-session', '');
15247
17069
  var output = pane.querySelector('.terminal-output');
15248
- if (output) output.innerHTML += '\r\n<span style="color:var(--text-dim);opacity:0.5;">[Process exited code=' + code + ']</span>';
17070
+ if (output) window.appendTerminalOutput(output, '\r\n[Process exited code=' + code + ']', { color:'var(--text-dim)', opacity:'0.5' });
15249
17071
  });
15250
17072
  }
15251
17073
 
@@ -15257,7 +17079,7 @@ if (api.onTerminalTakeover) {
15257
17079
  if (api.terminalTakeoverState) window.refreshTerminalTakeoverPanes();
15258
17080
 
15259
17081
  // === Scroll to Bottom ===
15260
- var _chatScrollNearBottomPx = 80;
17082
+ var _chatScrollNearBottomPx = 24;
15261
17083
  var _chatShouldAutoScroll = true;
15262
17084
  function shouldAutoScroll(el) { return el.scrollHeight - el.scrollTop - el.clientHeight <= _chatScrollNearBottomPx; }
15263
17085
  function autoScrollIfAtBottom() { var el = els['chat-area']; if (!el) return; if (_chatShouldAutoScroll) el.scrollTop = el.scrollHeight; }
@@ -15547,6 +17369,7 @@ window.openSubWin = function(title, html) {
15547
17369
  els['sub-win-title'].textContent = title;
15548
17370
  els['sub-win-body'].innerHTML = html;
15549
17371
  els['sub-win'].classList.toggle('memory-lab-window', !!(state.activeSubWindowView && state.activeSubWindowView.name === 'memoryLab'));
17372
+ els['sub-win'].classList.toggle('settings-window', !!(state.activeSubWindowView && state.activeSubWindowView.name === 'settings'));
15550
17373
  els['sub-win-overlay'].classList.add('open');
15551
17374
  updateApplicationInertState();
15552
17375
  requestAnimationFrame(focusSubWindowInitial);
@@ -15577,6 +17400,7 @@ window.closeSubWin = function() {
15577
17400
  els['sub-win-title'].textContent = previous.title;
15578
17401
  els['sub-win-body'].innerHTML = previous.html;
15579
17402
  els['sub-win'].classList.toggle('memory-lab-window', !!(previous.view && previous.view.name === 'memoryLab'));
17403
+ els['sub-win'].classList.toggle('settings-window', !!(previous.view && previous.view.name === 'settings'));
15580
17404
  requestAnimationFrame(function() {
15581
17405
  els['sub-win-body'].scrollTop = previous.scrollTop || 0;
15582
17406
  state.restoringSubWindow = false;
@@ -15587,7 +17411,7 @@ window.closeSubWin = function() {
15587
17411
  els['sub-win-overlay'].classList.remove('open');
15588
17412
  state.activeSubWindowView = null;
15589
17413
  var win = els['sub-win'];
15590
- if (win) { win.classList.remove('memory-lab-window'); win.style.left = ''; win.style.top = ''; win.style.margin = ''; }
17414
+ if (win) { win.classList.remove('memory-lab-window', 'settings-window'); win.style.left = ''; win.style.top = ''; win.style.margin = ''; }
15591
17415
  var restoreFocus = subWindowOriginFocus;
15592
17416
  subWindowOriginFocus = null;
15593
17417
  updateApplicationInertState();
@@ -15660,7 +17484,10 @@ window.openSettings = function(tab) {
15660
17484
  if (refreshingSettings) state.restoringSubWindow = true;
15661
17485
  window.openSubWin(t('settings.title'), html);
15662
17486
  state.restoringSubWindow = false;
15663
- requestAnimationFrame(function() { window.settingsTab(activeTab); });
17487
+ requestAnimationFrame(function() {
17488
+ window.settingsTab(activeTab);
17489
+ wireLiquidRailInteractions(document.querySelector('.settings-tabs:not(.plugin-tabs)'), '.stab-btn[data-stab]');
17490
+ });
15664
17491
  };
15665
17492
 
15666
17493
  function renderGeneralSettings() {
@@ -15740,7 +17567,7 @@ function renderGeneralSettings() {
15740
17567
  '</div>' +
15741
17568
  '<div class="setting-row">' +
15742
17569
  '<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)">' +
17570
+ '<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
17571
  '<div class="setting-desc" id="glass-desc" aria-live="polite">' + esc(glassDescription) + '</div></div>' +
15745
17572
  '</div>' +
15746
17573
  '<div class="setting-row">' +
@@ -16240,6 +18067,7 @@ function glassPresentationForOpacity(value) {
16240
18067
  var alpha = opacityPercent / 100;
16241
18068
  var transparencyPercent = 100 - opacityPercent;
16242
18069
  var glassWidth = 20 * transparencyPercent / 100;
18070
+ var opticalStrength = Math.min(2, transparencyPercent / 15);
16243
18071
  var rounded = function(number) { return Math.round(number * 10000) / 10000; };
16244
18072
  return {
16245
18073
  opacityPercent: rounded(opacityPercent),
@@ -16248,6 +18076,12 @@ function glassPresentationForOpacity(value) {
16248
18076
  blur1: rounded(0.4 * glassWidth),
16249
18077
  blur2: rounded(0.8 * glassWidth),
16250
18078
  blur3: rounded(glassWidth),
18079
+ carrierBlur: rounded(glassWidth * 2.2),
18080
+ carrierAlpha: rounded(Math.max(0.08, 0.44 * alpha)),
18081
+ floatRefractionHeight: rounded(12 * opticalStrength),
18082
+ floatRefractionAmount: rounded(24 * opticalStrength),
18083
+ floatChromaticAberration: rounded(opticalStrength),
18084
+ floatOpticalOpacity: rounded(Math.min(1, opticalStrength)),
16251
18085
  alpha1: rounded(0.75 * alpha),
16252
18086
  alpha2: rounded(0.80 * alpha),
16253
18087
  alpha3: rounded(0.85 * alpha),
@@ -16280,11 +18114,21 @@ function applyGlassPresentation(presentation) {
16280
18114
  root.style.setProperty('--glass-blur-2', presentation.blur2 + 'px');
16281
18115
  root.style.setProperty('--glass-blur-3', presentation.blur3 + 'px');
16282
18116
  }
18117
+ root.style.setProperty('--carrier-glass-blur', presentation.carrierBlur + 'px');
18118
+ root.style.setProperty('--carrier-glass-alpha', String(presentation.carrierAlpha));
18119
+ root.style.setProperty('--liquid-float-refraction-amount', presentation.floatRefractionAmount + 'px');
18120
+ root.style.setProperty('--liquid-float-chromatic-aberration', String(presentation.floatChromaticAberration));
18121
+ root.style.setProperty('--liquid-float-optical-opacity', String(presentation.floatOpticalOpacity));
16283
18122
  var description = glassDescriptionText(presentation);
16284
18123
  var desc = document.getElementById('glass-desc');
16285
18124
  if (desc) desc.textContent = description;
16286
18125
  var slider = document.getElementById('settings-glass-opacity');
16287
- if (slider) slider.setAttribute('aria-valuetext', description);
18126
+ if (slider) {
18127
+ slider.setAttribute('aria-valuetext', description);
18128
+ slider.style.setProperty('--liquid-range-progress', presentation.opacityPercent + '%');
18129
+ }
18130
+ var valueOutput = document.getElementById('settings-glass-value');
18131
+ if (valueOutput) valueOutput.textContent = glassNumberText(presentation.opacityPercent) + '%';
16288
18132
  return presentation;
16289
18133
  }
16290
18134
 
@@ -19431,14 +21275,25 @@ function pendingConversationActivation(target) {
19431
21275
  }
19432
21276
 
19433
21277
  function applyBackendConversations(items, activeId, workspaceId) {
19434
- if (!state.currentWorkspace || !items || !items.length) return;
21278
+ if (!state.currentWorkspace) return;
19435
21279
  var key = currentWorkspaceKey(workspaceId);
19436
21280
  var isForegroundWorkspace = key === currentWorkspaceKey();
19437
21281
  var existingConversations = state.workspaceConversations[key] || [];
19438
- var convs = [];
19439
- var requestedActiveId = String(activeId || '');
19440
21282
  var holdId = String(state.foregroundConversationHoldId || '');
19441
21283
  var useHold = !!(isForegroundWorkspace && holdId && Date.now() < Number(state.foregroundConversationHoldUntil || 0));
21284
+ if ((!items || !items.length) && !useHold) {
21285
+ // 后端权威列表为空:工作区对话确实已清空。保持为空,并移除本地
21286
+ // 自动兜底的 default 对话,跨重启/切换工作区后也不会复活。
21287
+ state.workspaceConversations[key] = [];
21288
+ state.workspaceActiveConversation[key] = -1;
21289
+ if (isForegroundWorkspace) {
21290
+ state.conversations = [];
21291
+ state.activeConversation = -1;
21292
+ }
21293
+ return;
21294
+ }
21295
+ var convs = [];
21296
+ var requestedActiveId = String(activeId || '');
19442
21297
  var seenIds = {};
19443
21298
  for (var i = 0; i < items.length; i++) {
19444
21299
  var item = items[i] || {};
@@ -19564,16 +21419,244 @@ function appendDomNodesInBatches(container, nodes, batchKey, batchSize) {
19564
21419
  appendBatch();
19565
21420
  }
19566
21421
 
21422
+ var conversationGlassFlight = { float: null, source: null, covered: null, timer: 0, generation: 0 };
21423
+ var conversationNativeDrag = {
21424
+ row: null,
21425
+ float: null,
21426
+ arrived: false,
21427
+ grabX: 0,
21428
+ grabY: 0,
21429
+ clientX: 0,
21430
+ clientY: 0
21431
+ };
21432
+
21433
+ function positionConversationGlassAroundRow(float, row) {
21434
+ if (!float || !row) return;
21435
+ var rect = row.getBoundingClientRect();
21436
+ var edge = liquidInteractionEdgePx();
21437
+ keepLiquidFloatExpansionInsideViewport(float, rect);
21438
+ float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
21439
+ float.style.left = (rect.left - edge) + 'px';
21440
+ float.style.top = (rect.top - edge) + 'px';
21441
+ float.style.width = (rect.width + edge * 2) + 'px';
21442
+ float.style.height = (rect.height + edge * 2) + 'px';
21443
+ if (float._renderKyantGlass) float._renderKyantGlass({
21444
+ left: rect.left - edge, top: rect.top - edge,
21445
+ width: rect.width + edge * 2, height: rect.height + edge * 2
21446
+ }, true);
21447
+ }
21448
+
21449
+ function followConversationDragPointer() {
21450
+ var drag = conversationNativeDrag;
21451
+ if (!drag.arrived || !drag.float) return;
21452
+ var list = els['conversation-list'];
21453
+ if (!list || !drag.row) return;
21454
+ var conversations = currentWorkspaceConversations();
21455
+ var draggedId = String(drag.row.getAttribute('data-conversation-id') || '');
21456
+ var draggedConversation = conversations.find(function(item) { return String(item && item.id || '') === draggedId; });
21457
+ var trackRows = Array.from(list.querySelectorAll('.conv-item')).filter(function(row) {
21458
+ var id = String(row.getAttribute('data-conversation-id') || '');
21459
+ var conversation = conversations.find(function(item) { return String(item && item.id || '') === id; });
21460
+ return conversation && draggedConversation && !!conversation.pinned === !!draggedConversation.pinned;
21461
+ });
21462
+ if (!trackRows.length) trackRows = [drag.row];
21463
+ var firstRect = trackRows[0].getBoundingClientRect();
21464
+ var lastRect = trackRows[trackRows.length - 1].getBoundingClientRect();
21465
+ var rowRect = drag.row.getBoundingClientRect();
21466
+ var projectedTop = drag.clientY - drag.grabY;
21467
+ var minTop = firstRect.top - rowRect.height;
21468
+ var maxTop = Math.max(minTop, lastRect.bottom);
21469
+ var trackTop = Math.max(minTop, Math.min(maxTop, projectedTop));
21470
+ var edge = liquidInteractionEdgePx();
21471
+ drag.float.style.left = (rowRect.left - edge) + 'px';
21472
+ drag.float.style.top = (trackTop - edge) + 'px';
21473
+ if (drag.float._renderKyantGlass) drag.float._renderKyantGlass();
21474
+ }
21475
+
21476
+ function wrapConversationDragContent(row, float) {
21477
+ if (!row || !float || !row.isConnected) return;
21478
+ var content = document.createElement('div');
21479
+ content.className = 'conversation-drag-content';
21480
+ Array.prototype.slice.call(row.childNodes).forEach(function(node) { content.appendChild(node.cloneNode(true)); });
21481
+ float.appendChild(content);
21482
+ row.classList.add('dragging', 'conversation-glass-dragging');
21483
+ float.classList.add('conversation-drag-following');
21484
+ conversationNativeDrag.arrived = true;
21485
+ conversationNativeDrag.float = float;
21486
+ followConversationDragPointer();
21487
+ }
21488
+
21489
+ window.updateConversationNativeDrag = function(clientX, clientY) {
21490
+ conversationNativeDrag.clientX = clientX;
21491
+ conversationNativeDrag.clientY = clientY;
21492
+ var drag = conversationNativeDrag;
21493
+ var list = els['conversation-list'];
21494
+ if (drag.row && list) {
21495
+ var conversations = currentWorkspaceConversations();
21496
+ var draggedId = String(drag.row.getAttribute('data-conversation-id') || '');
21497
+ var draggedConversation = conversations.find(function(item) { return String(item && item.id || '') === draggedId; });
21498
+ var peers = Array.from(list.querySelectorAll('.conv-item')).filter(function(row) {
21499
+ if (row === drag.row) return false;
21500
+ var id = String(row.getAttribute('data-conversation-id') || '');
21501
+ var conversation = conversations.find(function(item) { return String(item && item.id || '') === id; });
21502
+ return conversation && draggedConversation && !!conversation.pinned === !!draggedConversation.pinned;
21503
+ });
21504
+ if (peers.length) {
21505
+ var firstPeerRect = peers[0].getBoundingClientRect();
21506
+ var lastPeerRect = peers[peers.length - 1].getBoundingClientRect();
21507
+ if (clientY <= firstPeerRect.top) {
21508
+ list.insertBefore(drag.row, peers[0]);
21509
+ } else if (clientY >= lastPeerRect.bottom) {
21510
+ list.insertBefore(drag.row, peers[peers.length - 1].nextSibling);
21511
+ }
21512
+ }
21513
+ }
21514
+ followConversationDragPointer();
21515
+ };
21516
+
21517
+ // Native conversation drags can leave the list while the mouse button remains
21518
+ // held. Track at document capture scope so the carried glass never freezes at
21519
+ // the list boundary; drop/reorder ownership remains scoped to the list itself.
21520
+ document.addEventListener('dragover', function(event) {
21521
+ if (conversationNativeDrag.row) window.updateConversationNativeDrag(event.clientX, event.clientY);
21522
+ }, true);
21523
+
21524
+ window.cancelConversationGlassFlight = function() {
21525
+ conversationGlassFlight.generation += 1;
21526
+ if (conversationGlassFlight.timer) clearTimeout(conversationGlassFlight.timer);
21527
+ conversationGlassFlight.timer = 0;
21528
+ if (conversationGlassFlight.source) conversationGlassFlight.source.classList.remove('liquid-selection-source');
21529
+ conversationGlassFlight.source = null;
21530
+ if (conversationGlassFlight.covered) conversationGlassFlight.covered.classList.remove('liquid-selection-covered');
21531
+ conversationGlassFlight.covered = null;
21532
+ stopLiquidMotionTracking(conversationGlassFlight.float);
21533
+ if (conversationGlassFlight.float && conversationGlassFlight.float._releaseKyantGlass) {
21534
+ conversationGlassFlight.float._releaseKyantGlass();
21535
+ }
21536
+ if (conversationGlassFlight.float && conversationGlassFlight.float.parentNode) {
21537
+ conversationGlassFlight.float.parentNode.removeChild(conversationGlassFlight.float);
21538
+ }
21539
+ conversationGlassFlight.float = null;
21540
+ if (conversationNativeDrag.row) conversationNativeDrag.row.classList.remove('dragging', 'conversation-glass-dragging');
21541
+ conversationNativeDrag.row = null;
21542
+ conversationNativeDrag.float = null;
21543
+ conversationNativeDrag.arrived = false;
21544
+ window.setLiquidSidebarGestureLock('conversation-flight', false);
21545
+ };
21546
+
21547
+ window.animateConversationGlassTo = function(target, options) {
21548
+ options = options || {};
21549
+ var list = els['conversation-list'];
21550
+ if (!list || !target) { if (options.onArrive) options.onArrive(); return; }
21551
+ var source = list.querySelector('.conv-item.active') || target;
21552
+ var float = conversationGlassFlight.float;
21553
+ var redirecting = !!(float && float.isConnected);
21554
+ if (!redirecting) {
21555
+ float = document.createElement('div');
21556
+ float.className = 'liquid-selection-float conversation-selection-float liquid-selection-primed';
21557
+ mountLiquidViewportFloat(float);
21558
+ captureLiquidBackdropFrame().then(function(frame) {
21559
+ if (float && float.isConnected) attachKyantLiquidRenderer(float, frame);
21560
+ });
21561
+ conversationGlassFlight.float = float;
21562
+ conversationGlassFlight.source = source;
21563
+ source.classList.add('liquid-selection-source');
21564
+ }
21565
+ function place(row) {
21566
+ if (conversationGlassFlight.covered && conversationGlassFlight.covered !== row) {
21567
+ conversationGlassFlight.covered.classList.remove('liquid-selection-covered');
21568
+ }
21569
+ conversationGlassFlight.covered = row;
21570
+ row.classList.add('liquid-selection-covered');
21571
+ positionConversationGlassAroundRow(float, row);
21572
+ }
21573
+ var generation = ++conversationGlassFlight.generation;
21574
+ if (!redirecting) {
21575
+ place(source);
21576
+ showLiquidViewportFloat(float);
21577
+ place(source);
21578
+ // Lock the true source geometry before enabling lift and movement together.
21579
+ float.getBoundingClientRect();
21580
+ requestAnimationFrame(function() {
21581
+ if (conversationGlassFlight.generation !== generation || !float.isConnected) return;
21582
+ float.classList.remove('liquid-selection-primed');
21583
+ float.classList.add('visible');
21584
+ place(target);
21585
+ });
21586
+ window.setLiquidSidebarGestureLock('conversation-flight', true);
21587
+ } else {
21588
+ // Updating top/left while the transition is running makes CSS continue
21589
+ // from the currently rendered position; no second glass block is born.
21590
+ float.classList.remove('liquid-selection-landing');
21591
+ place(target);
21592
+ }
21593
+ if (conversationGlassFlight.timer) clearTimeout(conversationGlassFlight.timer);
21594
+ waitForLiquidSelectionArrival(float, function() {
21595
+ if (conversationGlassFlight.generation !== generation) return;
21596
+ conversationGlassFlight.timer = 0;
21597
+ if (options.dragging && target.isConnected) {
21598
+ wrapConversationDragContent(target, float);
21599
+ window.setLiquidSidebarGestureLock('conversation-flight', false);
21600
+ if (options.onArrive) options.onArrive();
21601
+ return;
21602
+ }
21603
+ landLiquidSelectionFloat(float, function() {
21604
+ if (conversationGlassFlight.generation !== generation) return;
21605
+ if (float._releaseKyantGlass) float._releaseKyantGlass();
21606
+ if (float.parentNode) float.parentNode.removeChild(float);
21607
+ conversationGlassFlight.float = null;
21608
+ if (conversationGlassFlight.source) conversationGlassFlight.source.classList.remove('liquid-selection-source');
21609
+ conversationGlassFlight.source = null;
21610
+ if (conversationGlassFlight.covered) conversationGlassFlight.covered.classList.remove('liquid-selection-covered');
21611
+ conversationGlassFlight.covered = null;
21612
+ window.setLiquidSidebarGestureLock('conversation-flight', false);
21613
+ if (options.onArrive) options.onArrive();
21614
+ });
21615
+ });
21616
+ };
21617
+
21618
+ window.activateConversationWithGlass = function(idx, row) {
21619
+ var conversations = currentWorkspaceConversations();
21620
+ if (!row || !conversations[idx]) {
21621
+ window.switchConversation(idx);
21622
+ return;
21623
+ }
21624
+ var targetId = String(conversations[idx].id || '');
21625
+ window.animateConversationGlassTo(row, { onArrive: function() {
21626
+ var current = currentWorkspaceConversations();
21627
+ var targetIndex = current.findIndex(function(item) { return String(item.id || '') === targetId; });
21628
+ if (targetIndex >= 0) window.switchConversation(targetIndex);
21629
+ } });
21630
+ };
21631
+
19567
21632
  function renderConversations() {
19568
21633
  var list = els['conversation-list'];
19569
21634
  if (!list) return;
19570
- list.innerHTML = '';
21635
+ if (list.dataset.conversationDropWired !== 'true') {
21636
+ list.dataset.conversationDropWired = 'true';
21637
+ list.addEventListener('dragover', function(event) {
21638
+ window.updateConversationNativeDrag(event.clientX, event.clientY);
21639
+ if (conversationNativeDrag.row) event.preventDefault();
21640
+ });
21641
+ list.addEventListener('drop', function(event) {
21642
+ if (!list.querySelector('.conv-item.dragging')) return;
21643
+ event.preventDefault();
21644
+ list.dataset.conversationDropCommitted = 'true';
21645
+ window.commitConversationDomOrder();
21646
+ });
21647
+ }
19571
21648
  if (!state.currentWorkspace) {
19572
21649
  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
21650
  updateWorkspaceGate();
19574
21651
  return;
19575
21652
  }
19576
21653
  var conversations = currentWorkspaceConversations();
21654
+ // 工作区对话被清空后保持为空:只显示空态提示,不自动创建对话。
21655
+ if (!conversations.length) {
21656
+ 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>';
21657
+ updateWorkspaceGate();
21658
+ return;
21659
+ }
19577
21660
  var summaryCounts = {};
19578
21661
  for (var ci = 0; ci < conversations.length; ci++) {
19579
21662
  var summaryKey = String(conversations[ci] && conversations[ci].summary || '');
@@ -19582,7 +21665,17 @@ function renderConversations() {
19582
21665
  var activeBeforeRender = (conversations.find(function(c) { return c && c.active; }) || {}).id;
19583
21666
  if (activeBeforeRender) setActiveWorkspaceConversationById(activeBeforeRender);
19584
21667
  else setActiveWorkspaceConversation(state.workspaceActiveConversation[currentWorkspaceKey()] || 0);
19585
- var conversationNodes = [];
21668
+ // 增量更新:复用已存在的行节点,就地更新 class 与内容。行节点不被替换,
21669
+ // 运行中对话的 marquee 跑马灯 CSS 动画就不会因重建 DOM 而从头开始。
21670
+ Array.prototype.slice.call(list.children).forEach(function(child) {
21671
+ if (!child || !child.classList || !child.classList.contains('conv-item')) child.parentNode.removeChild(child);
21672
+ });
21673
+ var existingRows = {};
21674
+ Array.prototype.slice.call(list.querySelectorAll('.conv-item')).forEach(function(row) {
21675
+ existingRows[String(row.getAttribute('data-conversation-id') || '')] = row;
21676
+ });
21677
+ var keepRows = {};
21678
+ var orderedRows = [];
19586
21679
  for (var i = 0; i < conversations.length; i++) {
19587
21680
  var conv = conversations[i];
19588
21681
  var runtimeKey = currentRuntimeKey(conv.id);
@@ -19591,53 +21684,114 @@ function renderConversations() {
19591
21684
  if (summaryCounts[String(conv.summary || '')] > 1) {
19592
21685
  displaySummary += ' · ' + String(conv.id || '').slice(-8);
19593
21686
  }
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');
21687
+ var running = !!(runtimeState && ['running', 'stopping', 'force_restarting'].indexOf(String(runtimeState.status || '')) >= 0);
19602
21688
  var runtimeBadge = runtimeState && runtimeState.status && runtimeState.status !== 'idle'
19603
21689
  ? '<span class="conv-runtime-badge ' + escAttr(String(runtimeState.status)) + '">' + esc(String(runtimeState.status)) + '</span>' : '';
19604
21690
  var branchCommIcon = conv.branchCommunication
19605
21691
  ? '<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 +
21692
+ var innerKey = [String(displaySummary), String(conv.messageCount || ''), String(runtimeState && runtimeState.status || ''), conv.branchCommunication ? '1' : '0'].join('|');
21693
+ var nextInner = '<span class="conv-summary" title="' + escAttr(String(conv.id || '')) + '">' + esc(displaySummary) + (conv.messageCount ? ' (' + esc(String(conv.messageCount)) + ')' : '') + '</span>' + branchCommIcon + runtimeBadge +
19607
21694
  '<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);
21695
+ var div = existingRows[String(conv.id || '')];
21696
+ if (div) {
21697
+ keepRows[String(conv.id || '')] = true;
21698
+ } else {
21699
+ div = document.createElement('div');
21700
+ div.setAttribute('role', 'listitem');
21701
+ div.setAttribute('draggable', 'true');
21702
+ div.setAttribute('data-conversation-id', String(conv.id || ''));
21703
+ div.addEventListener('keydown', window.handleConversationKey);
21704
+ div.addEventListener('contextmenu', function(event) {
21705
+ event.preventDefault();
21706
+ event.stopPropagation();
21707
+ window.openConversationActionMenu(event.currentTarget, this.getAttribute('data-conversation-id') || '', { x: event.clientX, y: event.clientY });
21708
+ });
21709
+ div.addEventListener('dragstart', function(event) {
21710
+ window.cancelConversationGlassFlight();
21711
+ var rect = this.getBoundingClientRect();
21712
+ conversationNativeDrag.row = this;
21713
+ conversationNativeDrag.arrived = false;
21714
+ conversationNativeDrag.grabX = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
21715
+ conversationNativeDrag.grabY = Math.max(0, Math.min(rect.height, event.clientY - rect.top));
21716
+ conversationNativeDrag.clientX = event.clientX;
21717
+ conversationNativeDrag.clientY = event.clientY;
21718
+ if (event.dataTransfer) {
21719
+ var transparentDragImage = document.createElement('canvas');
21720
+ transparentDragImage.width = transparentDragImage.height = 1;
21721
+ document.body.appendChild(transparentDragImage);
21722
+ event.dataTransfer.setDragImage(transparentDragImage, 0, 0);
21723
+ setTimeout(function() { transparentDragImage.remove(); }, 0);
21724
+ }
21725
+ list.classList.add('conversation-reordering');
21726
+ list.dataset.conversationDropCommitted = 'false';
21727
+ window.setLiquidSidebarGestureLock('conversation-reorder', true);
21728
+ window.animateConversationGlassTo(this, { dragging: true });
21729
+ if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', this.getAttribute('data-conversation-id') || ''); }
21730
+ });
21731
+ div.addEventListener('drag', function(event) {
21732
+ if (conversationNativeDrag.row === this && (event.clientX || event.clientY)) {
21733
+ window.updateConversationNativeDrag(event.clientX, event.clientY);
21734
+ }
21735
+ });
21736
+ div.addEventListener('dragend', function() {
21737
+ var releasedRow = this;
21738
+ list.classList.remove('conversation-reordering');
21739
+ Array.from(list.querySelectorAll('.conv-item')).forEach(function(row) { row.classList.remove('drag-over'); });
21740
+ window.animateConversationGlassTo(releasedRow, { onArrive: function() {
21741
+ releasedRow.classList.remove('dragging', 'conversation-glass-dragging');
21742
+ conversationNativeDrag.row = null;
21743
+ conversationNativeDrag.float = null;
21744
+ conversationNativeDrag.arrived = false;
21745
+ window.setLiquidSidebarGestureLock('conversation-reorder', false);
21746
+ renderConversations();
21747
+ } });
21748
+ });
21749
+ div.addEventListener('dragover', function(event) {
21750
+ window.updateConversationNativeDrag(event.clientX, event.clientY);
21751
+ var dragging = list.querySelector('.conv-item.dragging');
21752
+ if (!dragging || dragging === this) return;
21753
+ var convsNow = currentWorkspaceConversations();
21754
+ var draggingConv = convsNow.find(function(item) { return String(item.id) === String(dragging.getAttribute('data-conversation-id')); });
21755
+ var targetConv = convsNow.find(function(item) { return String(item.id) === String(this.getAttribute('data-conversation-id')); }, this);
21756
+ if (!draggingConv || !targetConv || !!draggingConv.pinned !== !!targetConv.pinned) return;
21757
+ event.preventDefault();
21758
+ var targetRect = this.getBoundingClientRect();
21759
+ var after = event.clientY >= targetRect.top + targetRect.height / 2;
21760
+ var anchor = after ? this.nextSibling : this;
21761
+ if (anchor !== dragging) list.insertBefore(dragging, anchor);
21762
+ Array.from(list.querySelectorAll('.conv-item')).forEach(function(row) { row.classList.remove('drag-over'); });
21763
+ });
21764
+ div.addEventListener('dragleave', function() { this.classList.remove('drag-over'); });
21765
+ div.addEventListener('drop', function(event) {
21766
+ event.preventDefault();
21767
+ event.stopPropagation();
21768
+ this.classList.remove('drag-over');
21769
+ list.dataset.conversationDropCommitted = 'true';
21770
+ window.commitConversationDomOrder();
21771
+ });
21772
+ existingRows[String(conv.id || '')] = div;
21773
+ }
21774
+ div.className = 'conv-item' + (conv.active ? ' active' : '');
21775
+ if (running) div.classList.add('marquee-border');
21776
+ div.setAttribute('tabindex', conv.active ? '0' : '-1');
21777
+ div.setAttribute('aria-current', conv.active ? 'true' : 'false');
21778
+ if (div.getAttribute('data-inner-key') !== innerKey) {
21779
+ div.setAttribute('data-inner-key', innerKey);
21780
+ div.innerHTML = nextInner;
21781
+ }
21782
+ div.onclick = function(idx) { return function(event) { window.activateConversationWithGlass(idx, event.currentTarget); }; }(i);
21783
+ orderedRows.push(div);
19639
21784
  }
19640
- appendDomNodesInBatches(list, conversationNodes, 'conversation-list', 24);
21785
+ Object.keys(existingRows).forEach(function(id) {
21786
+ if (keepRows[id]) return;
21787
+ var stale = existingRows[id];
21788
+ if (stale && stale.parentNode === list) stale.parentNode.removeChild(stale);
21789
+ });
21790
+ orderedRows.forEach(function(row, idx) {
21791
+ var current = list.children[idx];
21792
+ if (current === row) return;
21793
+ list.insertBefore(row, current || null);
21794
+ });
19641
21795
  updateWorkspaceGate();
19642
21796
  }
19643
21797
 
@@ -19925,16 +22079,28 @@ window.archiveConv = function(conversationId) {
19925
22079
  var currentConvs = currentWorkspaceConversations();
19926
22080
  var currentIdx = currentConvs.findIndex(function(item) { return String(item && item.id || 'default') === targetId; });
19927
22081
  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 = '';
22082
+ if (!currentConvs.length) {
22083
+ // 工作区所有对话被清空后保持为空:不再自动创建 default 对话。
22084
+ for (var emptyI = 0; emptyI < currentConvs.length; emptyI++) currentConvs[emptyI].active = false;
22085
+ state.workspaceActiveConversation[workspaceKey] = -1;
22086
+ state.activeConversation = -1;
22087
+ state.conversations = currentConvs;
22088
+ if (priorActiveId === targetId) {
22089
+ state.activeBackendConversationId = '';
22090
+ if (els['chat-area']) els['chat-area'].innerHTML = '';
22091
+ if (window.renderFlowTakeover) window.renderFlowTakeover(false, '', { target: targetRuntime });
22092
+ }
22093
+ } else {
22094
+ var nextActiveId = priorActiveId && priorActiveId !== targetId ? priorActiveId : String(currentConvs[Math.min(Math.max(currentIdx, 0), currentConvs.length - 1)].id || 'default');
22095
+ var nextActiveIndex = Math.max(0, currentConvs.findIndex(function(item) { return String(item && item.id || '') === nextActiveId; }));
22096
+ for (var i = 0; i < currentConvs.length; i++) currentConvs[i].active = i === nextActiveIndex;
22097
+ state.workspaceActiveConversation[workspaceKey] = nextActiveIndex;
22098
+ state.activeConversation = nextActiveIndex;
22099
+ state.conversations = currentConvs;
22100
+ if (priorActiveId === targetId) {
22101
+ state.activeBackendConversationId = nextActiveId;
22102
+ if (els['chat-area']) els['chat-area'].innerHTML = '';
22103
+ }
19938
22104
  }
19939
22105
  if (state.flowTakeovers) {
19940
22106
  var archivedFlowKey = runtimeKeyFor(targetRuntime.workspaceId, targetRuntime.conversationId);
@@ -20138,6 +22304,7 @@ window.renderMemoryLab = function() {
20138
22304
  html += '</div>';
20139
22305
  panel.innerHTML = html;
20140
22306
  if (state.memoryLabSearchQuery) window.updateMemoryLabSearch(state.memoryLabSearchQuery);
22307
+ wireLiquidRailInteractions(panel.querySelector('.memory-lab-view-menu'), 'button[data-memory-view]');
20141
22308
  if (state.memoryLabView === 'overview') requestAnimationFrame(function() { window.initMemoryLabOverview(); });
20142
22309
  state.memoryLabNavDirection = 'fade';
20143
22310
  };
@@ -20146,8 +22313,8 @@ window.memoryLabTopbar = function(lab) {
20146
22313
  var view = state.memoryLabView || 'overview';
20147
22314
  return '<div class="memory-lab-panel"><div class="memory-lab-topbar">' +
20148
22315
  '<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>' +
22316
+ '<button data-memory-view="overview" class="' + (view === 'overview' ? 'active' : '') + '" onclick="window.switchMemoryLabView(\'overview\')">' + esc(t('memoryLab.overview')) + '</button>' +
22317
+ '<button data-memory-view="detail" class="' + (view === 'detail' ? 'active' : '') + '" onclick="window.switchMemoryLabView(\'detail\')">' + esc(t('memoryLab.detail')) + '</button>' +
20151
22318
  '</div>' +
20152
22319
  '<div class="memory-lab-search">' +
20153
22320
  '<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 +23052,7 @@ window.showPluginList = function(tab, options) {
20885
23052
  state.activeSubWindowView = { name: 'plugins', tab: requested };
20886
23053
  }
20887
23054
  window.syncPluginTabs(!pluginOpen || !!(options && options.focusTab));
23055
+ wireLiquidRailInteractions(document.querySelector('.plugin-tabs[role="tablist"]'), '.stab-btn[data-plugin-tab]');
20888
23056
  var panel = document.getElementById('plugin-panel');
20889
23057
  if (panel) panel.replaceChildren();
20890
23058
  if (requested === 'mcp') window.renderMcpManager();
@@ -21798,6 +23966,30 @@ window.renderLeftWsList = function() {
21798
23966
  var holder = document.createElement('div');
21799
23967
  holder.innerHTML = html;
21800
23968
  appendDomNodesInBatches(container, Array.from(holder.children), 'workspace-list', 24);
23969
+ wireLiquidRailInteractions(container, '.left-ws-item');
23970
+ };
23971
+
23972
+ window.commitConversationDomOrder = function() {
23973
+ var list = els['conversation-list'];
23974
+ var convs = currentWorkspaceConversations();
23975
+ if (!list || !convs.length) return;
23976
+ window.cancelConversationGlassFlight();
23977
+ var byId = {};
23978
+ convs.forEach(function(item) { byId[String(item.id)] = item; });
23979
+ var ids = Array.from(list.querySelectorAll('.conv-item')).map(function(row) {
23980
+ return String(row.getAttribute('data-conversation-id') || '');
23981
+ }).filter(function(id) { return !!byId[id]; });
23982
+ if (ids.length !== convs.length) return renderConversations();
23983
+ var activeId = activeConversationId();
23984
+ convs.splice.apply(convs, [0, convs.length].concat(ids.map(function(id) { return byId[id]; })));
23985
+ setActiveWorkspaceConversationById(activeId);
23986
+ renderConversations();
23987
+ Promise.resolve(api.reorderConversations && api.reorderConversations(ids)).then(function(saved) {
23988
+ if (!saved) throw new Error(t('conversation.reorderFailed'));
23989
+ }).catch(function() {
23990
+ showUiNotice(t('conversation.reorderFailed'), 'error', 'reorder-conversations');
23991
+ if (api.getState) api.getState(currentConversationTarget(activeId)).then(function(s) { if (s) applyWorkspaceStateFromBackend(s); });
23992
+ });
21801
23993
  };
21802
23994
 
21803
23995
  window.handleWorkspaceKey = function(event) {
@@ -21916,10 +24108,10 @@ window.showNewWorkspaceDialog = function() {
21916
24108
  state.activeSubWindowView = { name: 'workspaceNew' };
21917
24109
  var html = '<div style="padding:10px;">' +
21918
24110
  '<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>' +
24111
+ '<div class="workspace-type-pager" role="radiogroup" aria-label="' + escAttr(t('workspace.type')) + '">' +
24112
+ '<label class="workspace-type-option active"><input type="radio" name="ws-type" value="internal" checked onchange="window.toggleWsType()">' + esc(t('workspace.internal')) + '</label>' +
24113
+ '<label class="workspace-type-option"><input type="radio" name="ws-type" value="external" onchange="window.toggleWsType()">' + esc(t('workspace.external')) + '</label>' +
24114
+ '<label class="workspace-type-option"><input type="radio" name="ws-type" value="ssh" onchange="window.toggleWsType()">' + esc(t('workspace.ssh')) + '</label>' +
21923
24115
  '</div>' +
21924
24116
  '<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
24117
  '<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 +24136,7 @@ window.showNewWorkspaceDialog = function() {
21944
24136
  '<button class="sec-btn" onclick="window.closeSubWin()">' + esc(t('common.cancel')) + '</button>' +
21945
24137
  '</div></div>';
21946
24138
  window.openSubWin(t('workspace.new'), html);
24139
+ wireLiquidRailInteractions(document.querySelector('.workspace-type-pager'), '.workspace-type-option');
21947
24140
  setTimeout(function() {
21948
24141
  var inp = document.getElementById('new-ws-input');
21949
24142
  if (inp) inp.focus();
@@ -21955,6 +24148,11 @@ window.toggleWsType = function() {
21955
24148
  for (var i = 0; i < radios.length; i++) {
21956
24149
  if (radios[i].checked) { selected = radios[i].value; break; }
21957
24150
  }
24151
+ var options = document.querySelectorAll('.workspace-type-option');
24152
+ for (var oi = 0; oi < options.length; oi++) {
24153
+ var radio = options[oi].querySelector('input[name="ws-type"]');
24154
+ options[oi].classList.toggle('active', !!(radio && radio.checked));
24155
+ }
21958
24156
  var extPath = document.getElementById('ws-ext-path');
21959
24157
  var sshFields = document.getElementById('ws-ssh-fields');
21960
24158
  var nameLabel = document.getElementById('ws-name-label-dlg');
@@ -23058,6 +25256,7 @@ function schedulePostStartupUiRendering() {
23058
25256
  this.setAttribute('aria-valuenow', String(Math.round(size)));
23059
25257
  });
23060
25258
  handles[i].addEventListener('mousedown', function(e) {
25259
+ if (window.liquidSidebarGesturesLocked && window.liquidSidebarGesturesLocked()) return;
23061
25260
  e.preventDefault();
23062
25261
  var handle = this;
23063
25262
  var target = this.getAttribute('data-target');