newmark-agent 0.5.14 → 0.5.15

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.
Files changed (64) hide show
  1. package/dist/conversation-utility-host.bundle.cjs +3373 -1900
  2. package/dist/conversation-utility-host.js +1 -1
  3. package/dist/core/agent.d.ts +105 -14
  4. package/dist/core/agent.js +658 -104
  5. package/dist/core/agentKernel/agent.d.ts +1 -0
  6. package/dist/core/agentKernel/agent.js +9 -0
  7. package/dist/core/agentKernelDiagnostics.d.ts +4 -6
  8. package/dist/core/agentKernelDiagnostics.js +12 -9
  9. package/dist/core/agentKernelRunner.d.ts +5 -0
  10. package/dist/core/agentKernelRunner.js +236 -85
  11. package/dist/core/autoRouter.js +16 -3
  12. package/dist/core/conversationCommandState.d.ts +17 -0
  13. package/dist/core/conversationCommandState.js +140 -0
  14. package/dist/core/conversationKernel.d.ts +25 -4
  15. package/dist/core/conversationKernel.js +347 -83
  16. package/dist/core/conversationListEvent.d.ts +6 -0
  17. package/dist/core/conversationListEvent.js +14 -0
  18. package/dist/core/electronUtilityAgentClient.d.ts +4 -1
  19. package/dist/core/electronUtilityAgentClient.js +4 -4
  20. package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
  21. package/dist/core/electronUtilityRuntimePool.js +11 -5
  22. package/dist/core/installUpdate.js +41 -29
  23. package/dist/core/providerUsageAccounting.d.ts +47 -0
  24. package/dist/core/providerUsageAccounting.js +71 -0
  25. package/dist/core/requestContextEstimate.d.ts +18 -0
  26. package/dist/core/requestContextEstimate.js +54 -0
  27. package/dist/core/subagent.d.ts +89 -5
  28. package/dist/core/subagent.js +264 -41
  29. package/dist/core/subagentCommunication.d.ts +43 -0
  30. package/dist/core/subagentCommunication.js +167 -0
  31. package/dist/core/types.d.ts +32 -1
  32. package/dist/core/utilityAgentProtocol.d.ts +4 -0
  33. package/dist/core/workEventCoalescer.js +4 -1
  34. package/dist/core/wslAgentClient.d.ts +18 -5
  35. package/dist/core/wslAgentClient.js +79 -27
  36. package/dist/core/wslAgentProtocol.d.ts +7 -0
  37. package/dist/core/wslAgentRuntimePool.d.ts +8 -2
  38. package/dist/core/wslAgentRuntimePool.js +19 -6
  39. package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
  40. package/dist/core/wslRuntimeProcessTree.js +104 -0
  41. package/dist/llm/provider.d.ts +5 -12
  42. package/dist/llm/provider.js +148 -267
  43. package/dist/main.js +582 -356
  44. package/dist/preload.js +3 -3
  45. package/dist/providers/chat-completions.adapter.d.ts +2 -0
  46. package/dist/providers/chat-completions.adapter.js +84 -36
  47. package/dist/providers/provider-adapter.d.ts +2 -0
  48. package/dist/providers/provider-events.d.ts +23 -14
  49. package/dist/providers/provider-events.js +148 -43
  50. package/dist/providers/provider-headers.d.ts +7 -3
  51. package/dist/providers/provider-headers.js +109 -39
  52. package/dist/providers/provider-request-compat.d.ts +6 -0
  53. package/dist/providers/provider-request-compat.js +37 -0
  54. package/dist/providers/responses.adapter.d.ts +2 -0
  55. package/dist/providers/responses.adapter.js +192 -126
  56. package/dist/server.d.ts +9 -2
  57. package/dist/server.js +100 -24
  58. package/dist/tools/index.js +17 -1
  59. package/dist/ui/index.html +2374 -616
  60. package/dist/ui/lucide-sprite.svg +7 -0
  61. package/dist/ui/startup.html +6 -6
  62. package/dist/wsl-agent-host.bundle.cjs +3381 -1906
  63. package/dist/wsl-agent-host.js +9 -4
  64. package/package.json +23 -4
@@ -22,39 +22,39 @@ try {
22
22
 
23
23
  /* --- Design Tokens --- */
24
24
  :root {
25
- /* Deep space palette */
26
- --bg: #0a0a1a;
25
+ /* Neutral charcoal surfaces; chroma belongs to functional accents. */
26
+ --bg: #101010;
27
27
  --app-bg: var(--bg);
28
- --bg2: #11112a;
29
- --bg3: #1a1a38;
30
- --surface: #1e1e40;
31
- --surface2: #252550;
32
- --surface3: #2a2a58;
28
+ --bg2: #181818;
29
+ --bg3: #222222;
30
+ --surface: #292929;
31
+ --surface2: #303030;
32
+ --surface3: #383838;
33
33
 
34
34
  /* Text hierarchy */
35
- --text: #c8d0e8;
36
- --text-dim: #7880a0;
37
- --text-bright: #e4ecff;
35
+ --text: #cecece;
36
+ --text-dim: #949494;
37
+ --text-bright: #f2f2f2;
38
38
  --text-accent: #a0b8ff;
39
39
 
40
40
  /* Native select popups need an opaque palette; Windows does not composite glass behind them. */
41
41
  --select-color-scheme: dark;
42
- --select-popup-bg: #11162a;
43
- --select-popup-text: #eef2ff;
44
- --select-popup-group: #afc0ff;
42
+ --select-popup-bg: #181818;
43
+ --select-popup-text: #f2f2f2;
44
+ --select-popup-group: #cecece;
45
45
  --select-popup-selected-bg: #355fce;
46
46
  --select-popup-selected-text: #ffffff;
47
- --select-popup-disabled: #8f99b5;
47
+ --select-popup-disabled: #949494;
48
48
  --control-hover-bg: rgba(255,255,255,0.08);
49
49
 
50
50
  /* Native editor palette */
51
- --editor-bg: #0b0d14;
51
+ --editor-bg: #141414;
52
52
  --editor-gutter-bg: rgba(255,255,255,0.025);
53
- --editor-ghost-text: rgba(205,211,222,0.48);
53
+ --editor-ghost-text: rgba(206,206,206,0.48);
54
54
  --editor-caret: #ffffff;
55
55
  --editor-selection: rgba(91,120,255,0.35);
56
- --editor-completion-bg: rgba(12,14,22,0.97);
57
- --editor-comment: #8b949e;
56
+ --editor-completion-bg: rgba(24,24,24,0.97);
57
+ --editor-comment: #949494;
58
58
  --editor-string: #9ecbff;
59
59
  --editor-number: #79b8ff;
60
60
  --editor-keyword: #f97583;
@@ -63,14 +63,14 @@ try {
63
63
  --editor-attr: #ffab70;
64
64
  --editor-heading: #79b8ff;
65
65
  --editor-code: #9ecbff;
66
- --editor-punct: #a8b0bd;
67
- --review-bg: rgba(18,20,28,.94);
66
+ --editor-punct: #b0b0b0;
67
+ --review-bg: rgba(24,24,24,.94);
68
68
  --review-row-hover: rgba(255,255,255,.035);
69
69
  --review-add-bg: rgba(56,212,160,.13);
70
70
  --review-add-text: #72e6b5;
71
71
  --review-del-bg: rgba(255,68,68,.13);
72
72
  --review-del-text: #ff9aa2;
73
- --notice-bg: rgba(18,20,28,.96);
73
+ --notice-bg: rgba(24,24,24,.96);
74
74
  --notice-shadow: 0 10px 36px rgba(0,0,0,.32);
75
75
  --notice-error-border: rgba(255,105,120,.55);
76
76
  --notice-error-text: #ffb0b8;
@@ -80,13 +80,13 @@ 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
+ --chat-canvas: var(--app-bg);
84
84
 
85
85
  /* Glass layers */
86
86
  --glass-alpha: 0.85;
87
- --glass-rgb-1: 10 10 26;
88
- --glass-rgb-2: 17 17 42;
89
- --glass-rgb-3: 26 26 56;
87
+ --glass-rgb-1: 16 16 16;
88
+ --glass-rgb-2: 24 24 24;
89
+ --glass-rgb-3: 34 34 34;
90
90
  --modal-surface: rgb(var(--glass-rgb-2) / max(var(--glass-alpha-2), 0.9));
91
91
  --glass-alpha-1: 0.6375;
92
92
  --glass-alpha-2: 0.68;
@@ -98,15 +98,14 @@ try {
98
98
  --glass-border-2: rgba(255,255,255,0.08);
99
99
  --glass-border-3: rgba(255,255,255,0.10);
100
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). */
101
+ /* Uniform frosted surfaces. Depth belongs to a narrow optical rim and the
102
+ real backdrop, never to a painted light/dark plane across the surface. */
104
103
  --liquid-edge-glow: rgba(255,255,255,0.10);
105
104
  --liquid-edge-glow-strong: rgba(255,255,255,0.16);
106
105
  --liquid-top-highlight: inset 0 1px 0 rgba(255,255,255,0.10);
107
106
  --liquid-inner-shadow: inset 0 -1px 0 rgba(0,0,0,0.22);
108
107
  --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));
108
+ --liquid-refraction: rgba(255,255,255,0.025);
110
109
 
111
110
  /* Borders & corners */
112
111
  --border: rgba(255,255,255,0.06);
@@ -117,6 +116,8 @@ try {
117
116
  --radius-lg: 16px;
118
117
  --radius-xl: 24px;
119
118
  --radius-full: 9999px;
119
+ /* List shells surround a 34px capsule with 7px padding and a 1px rim. */
120
+ --liquid-list-popup-radius: 25px;
120
121
 
121
122
  /* Marquee gradient */
122
123
  --g1: #000000;
@@ -150,6 +151,33 @@ try {
150
151
  --liquid-float-refraction-amount: 24px;
151
152
  --liquid-float-chromatic-aberration: 1;
152
153
  --liquid-float-optical-opacity: 1;
154
+ /* Popup tint is spatially uniform; the shared opacity/blur controls still
155
+ govern the material. Menu selection blocks keep their own flat fill. */
156
+ --liquid-popup-rgb: 28 28 28;
157
+ --liquid-popup-alpha: calc(var(--glass-alpha-2) * 0.82);
158
+ --liquid-popup-surface: rgb(var(--liquid-popup-rgb) / var(--liquid-popup-alpha));
159
+ --liquid-popup-blur: calc(var(--glass-blur-3) * 4);
160
+ --liquid-popup-saturation: 1.08;
161
+ --liquid-popup-rim: rgba(255,255,255,0.12);
162
+ --liquid-popup-inner-rim: rgba(255,255,255,0.045);
163
+ --liquid-popup-shadow: 0 12px 32px rgba(0,0,0,0.24), 0 2px 8px rgba(0,0,0,0.16);
164
+ --liquid-popup-refraction-height: 9px;
165
+ --liquid-popup-refraction-amount: 30px;
166
+ --liquid-popup-chromatic-aberration: 1.3;
167
+ --liquid-popup-light-opacity: 0;
168
+ --liquid-light-x: 50%;
169
+ --liquid-light-y: 50%;
170
+ /* Interaction glow is a physical-size light spot, independent of the
171
+ popup's dimensions. Keep the same CSS radius for small menus and large
172
+ settings dialogs; the carrier surface clips it to the popup shape. */
173
+ --liquid-popup-glow-radius: 20px;
174
+ --liquid-popup-stretch: 0;
175
+ --liquid-popup-squash: 0;
176
+ /* Desktop glass keeps the optical pass readable while limiting the
177
+ expensive backdrop region and avoiding oversized bloom on large dialogs. */
178
+ --liquid-popup-edge-width: 1px;
179
+ --liquid-popup-bloom: 0.16;
180
+ --liquid-popup-chromatic-shift: 1px;
153
181
 
154
182
  /* Transitions */
155
183
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
@@ -232,7 +260,10 @@ try {
232
260
  --glass-rgb-2: 255 255 255;
233
261
  --glass-rgb-3: 255 255 255;
234
262
  --modal-surface: rgba(248,250,255,0.82);
235
- --settings-modal-surface: rgba(255,255,255,0.94);
263
+ --liquid-popup-rgb: 248 250 253;
264
+ --liquid-popup-rim: rgba(70,82,105,0.13);
265
+ --liquid-popup-inner-rim: rgba(255,255,255,0.65);
266
+ --liquid-popup-shadow: 0 12px 32px rgba(35,45,65,0.10), 0 2px 8px rgba(35,45,65,0.06);
236
267
  --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);
237
268
  --glass-border-1: rgba(0,0,0,0.06);
238
269
  --glass-border-2: rgba(0,0,0,0.08);
@@ -298,8 +329,8 @@ html, body {
298
329
  align-items: center;
299
330
  justify-content: center;
300
331
  overflow: hidden;
301
- background: #0a0a1a;
302
- color: #f8fafc;
332
+ background: #101010;
333
+ color: #f2f2f2;
303
334
  font-family: 'Segoe UI', Arial, sans-serif;
304
335
  }
305
336
 
@@ -326,9 +357,9 @@ html, body {
326
357
 
327
358
  .startup-cover-copy { min-width: 0; flex: 1; padding-top: 1px; }
328
359
  .startup-cover-title { font-size: 18px; font-weight: 700; }
329
- .startup-cover-status { margin-top: 7px; color: #c4ccda; font-size: 12px; line-height: 1.45; }
330
- .startup-cover-detail { margin-top: 4px; color: #768197; font-size: 11px; line-height: 1.4; }
331
- .startup-cover-bar { height: 3px; margin-top: 12px; border-radius: 99px; background: #1c2638; overflow: hidden; }
360
+ .startup-cover-status { margin-top: 7px; color: #cecece; font-size: 12px; line-height: 1.45; }
361
+ .startup-cover-detail { margin-top: 4px; color: #949494; font-size: 11px; line-height: 1.4; }
362
+ .startup-cover-bar { height: 3px; margin-top: 12px; border-radius: 99px; background: #303030; overflow: hidden; }
332
363
  .startup-cover-bar > i { display: block; height: 100%; width: 38%; border-radius: inherit; background: linear-gradient(90deg, #36d399, #60a5fa); }
333
364
 
334
365
  @keyframes startup-cover-pulse {
@@ -1204,7 +1235,7 @@ button.left-ws-item {
1204
1235
  overflow: auto;
1205
1236
  padding: 6px;
1206
1237
  border: 1px solid var(--glass-border-2);
1207
- border-radius: var(--radius-xl);
1238
+ border-radius: var(--liquid-list-popup-radius);
1208
1239
  background: var(--modal-surface);
1209
1240
  box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
1210
1241
  }
@@ -2992,7 +3023,7 @@ button.todo-item { background: transparent; }
2992
3023
  .stack-card {
2993
3024
  width: calc(100% - 40px);
2994
3025
  margin: 0 20px;
2995
- background: rgba(18,20,34,0.92);
3026
+ background: rgba(24,24,24,0.92);
2996
3027
  backdrop-filter: blur(var(--glass-blur-2)) saturate(125%);
2997
3028
  -webkit-backdrop-filter: blur(var(--glass-blur-2)) saturate(125%);
2998
3029
  border: 1px solid var(--glass-border-1);
@@ -3084,7 +3115,7 @@ button.todo-item { background: transparent; }
3084
3115
  border-bottom: none;
3085
3116
  border-radius: 8px 8px 0 0;
3086
3117
  border-color: rgba(91,120,255,0.24);
3087
- background: linear-gradient(90deg, rgba(91,120,255,0.10), rgba(18,20,34,0.94) 34%);
3118
+ background: linear-gradient(90deg, rgba(91,120,255,0.10), rgba(24,24,24,0.94) 34%);
3088
3119
  box-shadow: none;
3089
3120
  }
3090
3121
  [data-theme="light"] #goal-bar {
@@ -3114,7 +3145,7 @@ button.todo-item { background: transparent; }
3114
3145
  border-bottom: none;
3115
3146
  border-radius: 8px 8px 0 0;
3116
3147
  border-color: rgba(76,196,160,0.26);
3117
- background: linear-gradient(90deg, rgba(76,196,160,0.12), rgba(18,20,34,0.94) 34%);
3148
+ background: linear-gradient(90deg, rgba(76,196,160,0.12), rgba(24,24,24,0.94) 34%);
3118
3149
  box-shadow: none;
3119
3150
  }
3120
3151
  [data-theme="light"] #flow-prompt-bar {
@@ -3132,7 +3163,7 @@ button.todo-item { background: transparent; }
3132
3163
  }
3133
3164
  #flow-prompt-bar.paused {
3134
3165
  border-color: rgba(226,183,74,0.34);
3135
- background: linear-gradient(90deg, rgba(226,183,74,0.14), rgba(18,20,34,0.94) 34%);
3166
+ background: linear-gradient(90deg, rgba(226,183,74,0.14), rgba(24,24,24,0.94) 34%);
3136
3167
  }
3137
3168
  #flow-prompt-bar.paused .stack-row::before {
3138
3169
  background:#e2b74a;
@@ -3569,7 +3600,7 @@ button.todo-item { background: transparent; }
3569
3600
  overflow: auto;
3570
3601
  padding: 7px;
3571
3602
  border: 1px solid var(--glass-border-2);
3572
- border-radius: var(--radius-xl);
3603
+ border-radius: var(--liquid-list-popup-radius);
3573
3604
  background: var(--modal-surface);
3574
3605
  box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
3575
3606
  }
@@ -3595,6 +3626,10 @@ button.todo-item { background: transparent; }
3595
3626
  .model-select-menu-option:hover,
3596
3627
  .model-select-menu-option:focus-visible { background: var(--control-hover-bg); color: var(--text-bright); outline: none; }
3597
3628
  .model-select-menu-option.selected { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--text-bright); }
3629
+ .model-select-menu:has(> .liquid-menu-color-block) .model-select-menu-option.selected,
3630
+ .newmark-select-menu:has(> .liquid-menu-color-block) .newmark-select-option.selected {
3631
+ background: transparent;
3632
+ }
3598
3633
  .liquid-selection-source { background: transparent !important; border-color: transparent !important; }
3599
3634
  .liquid-selection-source::after { display: none !important; opacity: 0 !important; }
3600
3635
  .conv-item.liquid-selection-covered { border-color: transparent !important; }
@@ -3670,7 +3705,7 @@ button.todo-item { background: transparent; }
3670
3705
  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, translate 110ms var(--ease-out-expo), scale 80ms linear;
3671
3706
  will-change: top, left, width, height, transform;
3672
3707
  translate: var(--liquid-boundary-x) var(--liquid-boundary-y);
3673
- scale: calc(1 + var(--liquid-boundary-pull) * .06) calc(1 - var(--liquid-boundary-pull) * .025);
3708
+ scale: var(--liquid-boundary-scale-x, 1) var(--liquid-boundary-scale-y, 1);
3674
3709
  }
3675
3710
  .liquid-selection-float.visible { opacity: 1; --liquid-lift-scale: 1; }
3676
3711
  .liquid-selection-float.liquid-selection-landing {
@@ -3737,10 +3772,16 @@ button.todo-item { background: transparent; }
3737
3772
  .liquid-selection-canvas {
3738
3773
  position: absolute;
3739
3774
  inset: 0;
3775
+ z-index: 0;
3740
3776
  width: 100%;
3741
3777
  height: 100%;
3742
3778
  border-radius: inherit;
3743
3779
  pointer-events: none;
3780
+ mix-blend-mode: normal;
3781
+ }
3782
+ .liquid-selection-float > :not(.liquid-selection-canvas) {
3783
+ position: relative;
3784
+ z-index: 1;
3744
3785
  }
3745
3786
  @keyframes model-menu-in { from { opacity: 0; transform: translateY(5px) scale(.985); } to { opacity: 1; transform: none; } }
3746
3787
 
@@ -3778,7 +3819,7 @@ button.todo-item { background: transparent; }
3778
3819
  overflow: auto;
3779
3820
  padding: 7px;
3780
3821
  border: 1px solid var(--glass-border-2);
3781
- border-radius: var(--radius-lg);
3822
+ border-radius: var(--liquid-list-popup-radius);
3782
3823
  background: var(--modal-surface);
3783
3824
  box-shadow: var(--shadow-lg), var(--liquid-top-highlight), var(--liquid-inner-shadow);
3784
3825
  }
@@ -4002,7 +4043,7 @@ button.todo-item { background: transparent; }
4002
4043
  #submit-btn:active { transform: none; scale: 1; }
4003
4044
 
4004
4045
  #submit-btn.working {
4005
- background: linear-gradient(135deg, #3a3a60 0%, #4a4a70 100%);
4046
+ background: linear-gradient(135deg, #383838 0%, #484848 100%);
4006
4047
  border-color: var(--border);
4007
4048
  box-shadow: none;
4008
4049
  animation: btn-pulse 1.5s ease-in-out infinite;
@@ -4011,7 +4052,7 @@ button.todo-item { background: transparent; }
4011
4052
  #submit-btn.running-action {
4012
4053
  position: relative;
4013
4054
  overflow: visible;
4014
- background: rgba(14, 16, 24, 0.88);
4055
+ background: rgba(20, 20, 20, 0.88);
4015
4056
  border-color: rgba(255,255,255,0.08);
4016
4057
  box-shadow: 0 0 0 1px rgba(255,255,255,0.04), 0 2px 16px var(--accent-glow);
4017
4058
  animation: none;
@@ -4846,7 +4887,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4846
4887
  box-shadow: var(--modal-shadow);
4847
4888
  }
4848
4889
  [data-theme="light"] .sub-win.liquid-glass-carrier {
4849
- background: var(--settings-modal-surface) !important;
4890
+ background: var(--liquid-popup-surface) !important;
4850
4891
  }
4851
4892
  .memory-lab-search-results[hidden] {
4852
4893
  display: none;
@@ -5125,7 +5166,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5125
5166
  padding: 7px 9px;
5126
5167
  border-radius: var(--radius-md);
5127
5168
  border: 1px solid var(--glass-border-1);
5128
- background: rgba(15, 18, 32, 0.58);
5169
+ background: rgba(24, 24, 24, 0.58);
5129
5170
  color: var(--text-dim);
5130
5171
  font-size: 11px;
5131
5172
  overflow: hidden;
@@ -5164,10 +5205,13 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5164
5205
  }
5165
5206
  .memory-lab-overview-link {
5166
5207
  fill: none;
5167
- stroke: rgba(150, 168, 210, 0.24);
5208
+ stroke: rgba(168, 168, 168, 0.24);
5168
5209
  stroke-width: 1.05;
5169
5210
  stroke-linecap: round;
5170
5211
  }
5212
+ [data-theme="light"] .memory-lab-overview-link {
5213
+ stroke: rgba(150, 168, 210, 0.24);
5214
+ }
5171
5215
  .memory-lab-overview-link.hot {
5172
5216
  stroke: rgba(126, 220, 255, 0.78);
5173
5217
  stroke-width: 1.45;
@@ -5204,7 +5248,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5204
5248
  padding: 0 9px 0 7px;
5205
5249
  border-radius: 999px;
5206
5250
  border: 1px solid var(--glass-border-2);
5207
- background: rgba(20, 24, 40, 0.72);
5251
+ background: rgba(34, 34, 34, 0.72);
5208
5252
  color: var(--text);
5209
5253
  font: 11px var(--font-ui);
5210
5254
  transform: translate(-50%, -50%);
@@ -5275,7 +5319,7 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5275
5319
  padding: 8px 9px;
5276
5320
  border-radius: var(--radius-md);
5277
5321
  border: 1px solid var(--glass-border-1);
5278
- background: rgba(10, 13, 24, 0.84);
5322
+ background: rgba(20, 20, 20, 0.84);
5279
5323
  color: var(--text);
5280
5324
  box-shadow: var(--shadow-lg);
5281
5325
  pointer-events: none;
@@ -5753,9 +5797,9 @@ html.newmark-memory-overview-viewer body {
5753
5797
 
5754
5798
 
5755
5799
  /* ============================================================
5756
- LIQUID GLASS (Kyant AndroidLiquidGlass backdrop parity)
5757
- CSS equivalent of the library lens/edge refraction, top
5758
- highlight, inner shadow, outer shadow and vibrancy boost.
5800
+ FROSTED GLASS
5801
+ Uniform translucency and real backdrop blur. The neutral rim stays narrow;
5802
+ menu color-block motion never switches to a glass material.
5759
5803
  ============================================================ */
5760
5804
  .liquid-glass {
5761
5805
  position: relative;
@@ -5786,7 +5830,116 @@ html.newmark-memory-overview-viewer body {
5786
5830
  backdrop-filter: blur(calc(var(--glass-blur-3) * 1.5)) saturate(150%) brightness(1.04);
5787
5831
  -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 1.5)) saturate(150%) brightness(1.04);
5788
5832
  }
5789
- [data-theme='light'] .liquid-glass {
5833
+ @property --liquid-popup-outset {
5834
+ syntax: '<length>';
5835
+ inherits: true;
5836
+ initial-value: 0px;
5837
+ }
5838
+ .liquid-glass-popup {
5839
+ isolation: isolate;
5840
+ overflow: hidden;
5841
+ clip-path: inset(var(--liquid-popup-edge-y, 0px) var(--liquid-popup-edge-x, 0px) round var(--liquid-popup-radius, var(--radius-xl)));
5842
+ background: var(--liquid-popup-surface);
5843
+ border-color: var(--liquid-popup-rim);
5844
+ backdrop-filter: blur(var(--liquid-popup-blur)) saturate(var(--liquid-popup-saturation));
5845
+ -webkit-backdrop-filter: blur(var(--liquid-popup-blur)) saturate(var(--liquid-popup-saturation));
5846
+ box-shadow: inset 0 0 0 0.5px var(--liquid-popup-inner-rim), var(--liquid-popup-shadow);
5847
+ /* Only the optical boundary responds. Scaling this node also scales every
5848
+ text-bearing descendant and changes pointer-track measurements. */
5849
+ scale: 1;
5850
+ transition: clip-path 120ms var(--ease-out-expo), transform 120ms var(--ease-out-expo), --liquid-popup-outset 120ms var(--ease-out-expo);
5851
+ }
5852
+ .liquid-popup-optical-canvas {
5853
+ position: absolute;
5854
+ inset: 0 !important;
5855
+ z-index: 0;
5856
+ width: 100%;
5857
+ height: 100%;
5858
+ border-radius: inherit;
5859
+ pointer-events: none;
5860
+ /* This is the carrier surface pass, not a top-level screen overlay. */
5861
+ mix-blend-mode: normal;
5862
+ opacity: 1;
5863
+ transform: none !important;
5864
+ translate: none !important;
5865
+ scale: 1 !important;
5866
+ }
5867
+ .liquid-glass-popup > :not(.liquid-popup-optical-canvas) {
5868
+ position: relative;
5869
+ z-index: 1;
5870
+ }
5871
+ .liquid-glass-popup > .liquid-popup-optical-canvas {
5872
+ pointer-events: none;
5873
+ }
5874
+ /* The selected-option block is another carrier-surface pass. Keep it
5875
+ absolute and beneath option content; the generic popup content rule above
5876
+ must not promote it into the text layer or make it participate in layout. */
5877
+ .liquid-glass-popup > .liquid-menu-color-block {
5878
+ position: absolute;
5879
+ z-index: 0;
5880
+ }
5881
+ /* List popups are themselves the scroll surface. The shared popup shell
5882
+ clips optical passes, but its generic overflow:hidden must not shadow the
5883
+ max-height/overflow:auto contract declared by the menu components above. */
5884
+ .conv-action-menu.liquid-glass-popup,
5885
+ .model-select-menu.liquid-glass-popup,
5886
+ .newmark-select-menu.liquid-glass-popup {
5887
+ overflow-y: auto;
5888
+ overflow-x: hidden;
5889
+ }
5890
+ .model-select-menu.liquid-glass-popup,
5891
+ .conv-action-menu.liquid-glass-popup,
5892
+ .newmark-select-menu.liquid-glass-popup {
5893
+ --liquid-popup-radius: var(--liquid-list-popup-radius);
5894
+ }
5895
+ .command-surface.liquid-glass-popup,
5896
+ .sub-win.liquid-glass-popup {
5897
+ --liquid-popup-radius: var(--radius-xl);
5898
+ }
5899
+ #context-inspector.liquid-glass-popup {
5900
+ --liquid-popup-radius: var(--radius-lg);
5901
+ }
5902
+ .liquid-glass-popup.liquid-popup-exit {
5903
+ transform: scale(.78);
5904
+ transform-origin: var(--liquid-popup-origin-x, 50%) var(--liquid-popup-origin-y, 50%);
5905
+ opacity: 0;
5906
+ transition: transform 190ms var(--ease-out-expo), opacity 170ms var(--ease-out-expo), scale 190ms var(--ease-out-expo);
5907
+ }
5908
+ .liquid-glass-popup::before {
5909
+ /* The internal canvas owns touch light. No second CSS lamp or baked
5910
+ illumination plane is composited over the frosted material. */
5911
+ background: none;
5912
+ opacity: 0;
5913
+ mix-blend-mode: normal;
5914
+ }
5915
+ .liquid-glass-popup::after {
5916
+ inset: var(--liquid-popup-edge-y, 0px) var(--liquid-popup-edge-x, 0px);
5917
+ transition: inset 120ms var(--ease-out-expo);
5918
+ border: var(--liquid-popup-edge-width) solid var(--liquid-popup-rim);
5919
+ box-shadow: inset 0 0 0 0.5px var(--liquid-popup-inner-rim);
5920
+ }
5921
+ [data-theme='light'] .liquid-glass-popup {
5922
+ backdrop-filter: blur(var(--liquid-popup-blur)) saturate(var(--liquid-popup-saturation));
5923
+ -webkit-backdrop-filter: blur(var(--liquid-popup-blur)) saturate(var(--liquid-popup-saturation));
5924
+ box-shadow: inset 0 0 0 0.5px var(--liquid-popup-inner-rim), var(--liquid-popup-shadow);
5925
+ }
5926
+ [data-theme='light'] .liquid-glass-popup::before {
5927
+ opacity: 0;
5928
+ mix-blend-mode: normal;
5929
+ }
5930
+
5931
+ /* Keep motion and backdrop work bounded on reduced-motion / low-power PCs. */
5932
+ @media (prefers-reduced-motion: reduce) {
5933
+ .liquid-glass-popup,
5934
+ .liquid-glass-popup.liquid-popup-exit,
5935
+ .liquid-menu-color-block,
5936
+ .liquid-selection-float { transition-duration: 1ms !important; animation-duration: 1ms !important; }
5937
+ .liquid-glass-popup::before { opacity: 0; }
5938
+ }
5939
+ @media (max-width: 900px), (max-height: 680px) {
5940
+ .liquid-glass-popup { --liquid-popup-blur: calc(var(--glass-blur-3) * 3); }
5941
+ }
5942
+ [data-theme='light'] .liquid-glass:not(.liquid-glass-popup) {
5790
5943
  backdrop-filter: none;
5791
5944
  -webkit-backdrop-filter: none;
5792
5945
  }
@@ -5796,26 +5949,168 @@ html.newmark-memory-overview-viewer body {
5796
5949
  }
5797
5950
  .liquid-glass-carrier {
5798
5951
  background: rgb(var(--glass-rgb-2) / var(--carrier-glass-alpha)) !important;
5799
- backdrop-filter: blur(var(--carrier-glass-blur)) saturate(150%) brightness(1.02) !important;
5800
- -webkit-backdrop-filter: blur(var(--carrier-glass-blur)) saturate(150%) brightness(1.02) !important;
5952
+ backdrop-filter: blur(var(--carrier-glass-blur)) saturate(108%) !important;
5953
+ -webkit-backdrop-filter: blur(var(--carrier-glass-blur)) saturate(108%) !important;
5954
+ }
5955
+ .liquid-glass-popup.liquid-glass-carrier {
5956
+ background: var(--liquid-popup-surface) !important;
5957
+ backdrop-filter: blur(var(--liquid-popup-blur)) saturate(var(--liquid-popup-saturation)) !important;
5958
+ -webkit-backdrop-filter: blur(var(--liquid-popup-blur)) saturate(var(--liquid-popup-saturation)) !important;
5959
+ }
5960
+ /* A list popup IS the scroll container. Absolute pseudo-elements/canvases
5961
+ belong to its scrolling content and carry a second rounded shell away.
5962
+ Paint its rim and touch light on the scrollport's own background instead;
5963
+ native background-attachment:scroll pins this paint without scroll-event
5964
+ compensation, wrappers, or any changes to option geometry/gesture logic. */
5965
+ :is(.conv-action-menu, .model-select-menu, .newmark-select-menu).liquid-glass-popup {
5966
+ background-image: radial-gradient(circle var(--liquid-popup-glow-radius, 20px) at var(--liquid-light-x, 50%) var(--liquid-light-y, 50%), rgba(255,255,255,calc(.24 * var(--liquid-popup-light-opacity, 0))), rgba(255,255,255,0) 100%) !important;
5967
+ background-attachment: scroll;
5968
+ background-origin: padding-box;
5969
+ background-clip: padding-box;
5970
+ /* Clip-path owns inward click feedback within the 8px optical budget.
5971
+ Extend each inset rim to that boundary so its visible edge stays thin. */
5972
+ box-shadow:
5973
+ inset calc(var(--liquid-popup-edge-x, 0px) + 1px) 0 0 var(--liquid-popup-inner-rim),
5974
+ inset calc(-1 * (var(--liquid-popup-edge-x, 0px) + 1px)) 0 0 var(--liquid-popup-inner-rim),
5975
+ inset 0 calc(var(--liquid-popup-edge-y, 0px) + 1px) 0 var(--liquid-popup-inner-rim),
5976
+ inset 0 calc(-1 * (var(--liquid-popup-edge-y, 0px) + 1px)) 0 var(--liquid-popup-inner-rim),
5977
+ var(--liquid-popup-shadow);
5978
+ }
5979
+ :is(.conv-action-menu, .model-select-menu, .newmark-select-menu).liquid-glass-popup::before,
5980
+ :is(.conv-action-menu, .model-select-menu, .newmark-select-menu).liquid-glass-popup::after {
5981
+ content: none;
5982
+ }
5983
+ :is(.conv-action-menu, .model-select-menu, .newmark-select-menu).liquid-glass-popup > .liquid-popup-optical-canvas {
5984
+ display: none;
5985
+ }
5986
+ /* A held outward pull expands the actual frosted material, never the text or
5987
+ scroll container. Native backdrops stay below their own top-layer popover;
5988
+ other popups use a decorative sibling inside their existing overlay. */
5989
+ .liquid-glass-popup[data-liquid-popup-feedback='outward']::backdrop,
5990
+ .liquid-popup-feedback-surface {
5991
+ box-sizing: border-box;
5992
+ inset: auto;
5993
+ margin: 0;
5994
+ padding: 0;
5995
+ left: calc(var(--liquid-feedback-left) - var(--liquid-popup-outset) * var(--liquid-popup-pull-left, 0));
5996
+ top: calc(var(--liquid-feedback-top) - var(--liquid-popup-outset) * var(--liquid-popup-pull-top, 0));
5997
+ width: calc(var(--liquid-feedback-width) + var(--liquid-popup-outset) * (var(--liquid-popup-pull-left, 0) + var(--liquid-popup-pull-right, 0)));
5998
+ height: calc(var(--liquid-feedback-height) + var(--liquid-popup-outset) * (var(--liquid-popup-pull-top, 0) + var(--liquid-popup-pull-bottom, 0)));
5999
+ border: 1px solid var(--liquid-feedback-rim);
6000
+ /* Each corner follows only its adjacent force-facing sides. The opposite
6001
+ contour, including its corners, keeps the popup's original radius. */
6002
+ border-radius:
6003
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-left, 0))
6004
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-right, 0))
6005
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-right, 0))
6006
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-left, 0)) /
6007
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-top, 0))
6008
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-top, 0))
6009
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-bottom, 0))
6010
+ calc(var(--liquid-feedback-radius) + var(--liquid-popup-outset) * var(--liquid-popup-pull-bottom, 0));
6011
+ background: var(--liquid-feedback-color);
6012
+ backdrop-filter: var(--liquid-feedback-filter);
6013
+ -webkit-backdrop-filter: var(--liquid-feedback-filter);
6014
+ box-shadow: inset 0 0 0 .5px var(--liquid-feedback-inner-rim);
6015
+ pointer-events: none;
6016
+ }
6017
+ .liquid-popup-feedback-surface {
6018
+ position: absolute;
6019
+ transition: --liquid-popup-outset 120ms var(--ease-out-expo);
6020
+ }
6021
+ :root .liquid-glass-popup[data-liquid-popup-feedback='outward'] {
6022
+ /* The feedback surface owns this material once. Keeping another background
6023
+ or backdrop here would darken it and leave seams around the old outline. */
6024
+ background-color: transparent !important;
6025
+ border-color: transparent !important;
6026
+ backdrop-filter: none !important;
6027
+ -webkit-backdrop-filter: none !important;
6028
+ box-shadow: none !important;
6029
+ }
6030
+ .liquid-glass-popup[data-liquid-popup-feedback='outward']::after { opacity: 0; }
6031
+ @media (prefers-reduced-motion: reduce) {
6032
+ .liquid-popup-feedback-surface { transition-duration: 1ms !important; }
6033
+ }
6034
+ .model-select-menu-option,
6035
+ .newmark-select-option,
6036
+ .conv-action-menu-item,
6037
+ .command-option,
6038
+ .workspace-type-option {
6039
+ --liquid-option-x: 0px;
6040
+ --liquid-option-y: 0px;
6041
+ --liquid-option-press: 0;
6042
+ --liquid-option-origin-x: 50%;
6043
+ --liquid-option-origin-y: 50%;
6044
+ transform: translate(var(--liquid-option-x), var(--liquid-option-y)) scale(calc(1 + var(--liquid-option-press) * 0.035));
6045
+ transform-origin: var(--liquid-option-origin-x) var(--liquid-option-origin-y);
6046
+ transition: transform 110ms var(--ease-out-expo), background-color 110ms var(--ease-out-expo), filter 110ms var(--ease-out-expo);
6047
+ }
6048
+ .model-select-menu-option,
6049
+ .newmark-select-option {
6050
+ position: relative;
6051
+ z-index: 1;
6052
+ }
6053
+ /* Popup option content stays fixed; the carrier block owns movement and
6054
+ elastic response so text/icons never translate or scale during a drag. */
6055
+ .model-select-menu[data-liquid-direct-options="true"] > .model-select-menu-option,
6056
+ .newmark-select-menu[data-liquid-direct-options="true"] > .newmark-select-option {
6057
+ transform: none !important;
6058
+ scale: 1 !important;
6059
+ filter: none !important;
6060
+ }
6061
+ .liquid-glass-popup .conv-action-menu-item,
6062
+ .liquid-glass-popup .command-option,
6063
+ .liquid-glass-popup .workspace-type-option {
6064
+ transform: none !important;
6065
+ scale: 1 !important;
6066
+ }
6067
+ .liquid-menu-color-block {
6068
+ position: absolute;
6069
+ /* Inline left/top own the flight. An important inset shorthand overrides
6070
+ both coordinates and strands the block after the last option. */
6071
+ inset: auto;
6072
+ z-index: 0;
6073
+ pointer-events: none;
6074
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
6075
+ /* Match the original selected capsule silhouette at every menu width. */
6076
+ border-radius: var(--radius-full);
6077
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent);
6078
+ transition: left 240ms var(--ease-out-expo), top 240ms var(--ease-out-expo), width 240ms var(--ease-out-expo), height 240ms var(--ease-out-expo), opacity 80ms var(--ease-out-expo);
6079
+ transform: none !important;
6080
+ translate: none !important;
6081
+ scale: 1 !important;
6082
+ }
6083
+ .liquid-menu-color-block.liquid-block-lifted {
6084
+ /* This class is an interaction-state marker, not a material switch. Keep
6085
+ the base color, outline and content stacking for the entire journey. */
6086
+ backdrop-filter: none;
6087
+ -webkit-backdrop-filter: none;
6088
+ }
6089
+ .liquid-menu-color-block.liquid-block-lifted::after {
6090
+ content: none;
6091
+ }
6092
+ .model-select-menu-option.liquid-option-dragging,
6093
+ .newmark-select-option.liquid-option-dragging {
6094
+ filter: brightness(1.08) saturate(1.08);
6095
+ box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 5px 16px rgba(0,0,0,0.18);
5801
6096
  }
5802
6097
  /* List popups use the live carrier refraction only. The old static outer
5803
6098
  glow/shadow painted a second fixed background behind the popup. */
5804
- .conv-action-menu.liquid-glass,
5805
- .model-select-menu.liquid-glass,
5806
- .newmark-select-menu.liquid-glass {
6099
+ .conv-action-menu.liquid-glass:not(.liquid-glass-popup),
6100
+ .model-select-menu.liquid-glass:not(.liquid-glass-popup),
6101
+ .newmark-select-menu.liquid-glass:not(.liquid-glass-popup) {
5807
6102
  box-shadow: none;
5808
6103
  }
5809
- .conv-action-menu.liquid-glass::before,
5810
- .model-select-menu.liquid-glass::before,
5811
- .newmark-select-menu.liquid-glass::before {
6104
+ .conv-action-menu.liquid-glass:not(.liquid-glass-popup)::before,
6105
+ .model-select-menu.liquid-glass:not(.liquid-glass-popup)::before,
6106
+ .newmark-select-menu.liquid-glass:not(.liquid-glass-popup)::before {
5812
6107
  background: none;
5813
6108
  opacity: 0;
5814
6109
  mix-blend-mode: normal;
5815
6110
  }
5816
- .conv-action-menu.liquid-glass::after,
5817
- .model-select-menu.liquid-glass::after,
5818
- .newmark-select-menu.liquid-glass::after {
6111
+ .conv-action-menu.liquid-glass:not(.liquid-glass-popup)::after,
6112
+ .model-select-menu.liquid-glass:not(.liquid-glass-popup)::after,
6113
+ .newmark-select-menu.liquid-glass:not(.liquid-glass-popup)::after {
5819
6114
  box-shadow: none;
5820
6115
  }
5821
6116
  .liquid-selection-canvas { opacity: var(--liquid-float-optical-opacity); }
@@ -5877,6 +6172,11 @@ button:disabled { opacity: 0.5; }
5877
6172
  .shell-btn::after,
5878
6173
  .option-btn::after,
5879
6174
  .stack-icon-btn::after { display: none !important; }
6175
+ .model-select-menu-option::after,
6176
+ .newmark-select-option::after,
6177
+ .conv-action-menu-item::after,
6178
+ .command-option::after,
6179
+ .workspace-type-option::after { display: none !important; }
5880
6180
  #left-ws-add::after { display: none !important; }
5881
6181
  #left-ws-add:active,
5882
6182
  .left-collapse-btn:active { transform: none !important; scale: 1 !important; }
@@ -6328,6 +6628,13 @@ input[type="checkbox"].liquid-switch-dragging::before {
6328
6628
  <path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" />
6329
6629
  <path d="M7 3v4a1 1 0 0 0 1 1h7" />
6330
6630
  </symbol>
6631
+ <symbol id="scale" viewBox="0 0 24 24">
6632
+ <path d="M12 3v18" />
6633
+ <path d="m19 8 3 8a5 5 0 0 1-6 0zV7" />
6634
+ <path d="M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1" />
6635
+ <path d="m5 8 3 8a5 5 0 0 1-6 0zV7" />
6636
+ <path d="M7 21h10" />
6637
+ </symbol>
6331
6638
  <symbol id="scroll" viewBox="0 0 24 24">
6332
6639
  <path d="M19 17V5a2 2 0 0 0-2-2H4" />
6333
6640
  <path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" />
@@ -6593,13 +6900,13 @@ input[type="checkbox"].liquid-switch-dragging::before {
6593
6900
  <div class="model-select-shell" id="model-select-shell">
6594
6901
  <select class="model-select-native" id="model-select" tabindex="-1" aria-hidden="true"></select>
6595
6902
  <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>
6596
- <div class="model-select-menu liquid-glass liquid-glass-carrier" id="model-select-menu" role="listbox" aria-label="Model" popover="manual"></div>
6903
+ <div class="model-select-menu liquid-glass liquid-glass-carrier liquid-glass-popup" id="model-select-menu" role="listbox" aria-label="Model" popover="manual"></div>
6597
6904
  </div>
6598
- <div class="conv-action-menu liquid-glass liquid-glass-carrier" id="conversation-action-menu" role="menu" aria-label="Conversation actions" popover="manual"></div>
6905
+ <div class="conv-action-menu liquid-glass liquid-glass-carrier liquid-glass-popup" id="conversation-action-menu" role="menu" aria-label="Conversation actions" popover="manual"></div>
6599
6906
  <div class="context-token-wrap">
6600
6907
  <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>
6601
6908
  <div id="context-token-tooltip"></div>
6602
- <div id="context-inspector" role="dialog" aria-label="Context management" aria-live="polite"></div>
6909
+ <div id="context-inspector" class="liquid-glass liquid-glass-popup" role="dialog" aria-label="Context management" aria-live="polite"></div>
6603
6910
  </div>
6604
6911
  <select class="tool-select" id="intel-select">
6605
6912
  <option value="low">low</option>
@@ -6713,7 +7020,7 @@ input[type="checkbox"].liquid-switch-dragging::before {
6713
7020
 
6714
7021
  <!-- ========== SUB-WINDOW OVERLAY ========== -->
6715
7022
  <div class="sub-win-overlay" id="sub-win-overlay" onclick="if(event.target===this)window.closeSubWin()">
6716
- <div class="sub-win liquid-glass liquid-glass-carrier" id="sub-win" role="dialog" aria-modal="true" aria-labelledby="sub-win-title" tabindex="-1">
7023
+ <div class="sub-win liquid-glass liquid-glass-carrier liquid-glass-popup" id="sub-win" role="dialog" aria-modal="true" aria-labelledby="sub-win-title" tabindex="-1">
6717
7024
  <div class="sub-win-header" id="sub-win-header">
6718
7025
  <span class="sub-win-title" id="sub-win-title">Window</span>
6719
7026
  <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>
@@ -6722,7 +7029,7 @@ input[type="checkbox"].liquid-switch-dragging::before {
6722
7029
  </div>
6723
7030
  </div>
6724
7031
  <div class="command-surface-overlay" id="command-surface-overlay" onclick="if(event.target===this)window.closeCommandSurface()">
6725
- <section class="command-surface" id="command-surface" role="dialog" aria-modal="true" aria-labelledby="command-surface-title" tabindex="-1">
7032
+ <section class="command-surface liquid-glass liquid-glass-popup" id="command-surface" role="dialog" aria-modal="true" aria-labelledby="command-surface-title" tabindex="-1">
6726
7033
  <header class="command-surface-header">
6727
7034
  <div class="command-surface-heading">
6728
7035
  <div class="command-surface-title" id="command-surface-title"></div>
@@ -7625,9 +7932,17 @@ var NEWMARK_I18N = {
7625
7932
  'status.subagentRecords': 'active or retained agent record(s)',
7626
7933
  'status.contextCompressed': 'Context compressed',
7627
7934
  'status.contextCompression': 'Context compression',
7628
- 'status.contextTokens': 'Context tokens',
7629
- 'status.conversationTokens': 'Conversation tokens',
7630
- 'status.cacheHitRate': 'Cache hit rate',
7935
+ 'status.contextTokens': 'Context tokens (estimated)',
7936
+ 'status.conversationTokens': 'Conversation usage (reported)',
7937
+ 'status.cacheHitRate': 'Conversation cache hit rate',
7938
+ 'status.usageUnavailable': 'Not reported',
7939
+ 'status.cacheReportedPortion': 'Reported portion',
7940
+ 'status.lastReportedInput': 'Latest main request input (reported)',
7941
+ 'status.requestOverhead': 'System / tools (estimated)',
7942
+ 'status.requestOverheadExcluded': 'Not included in history estimate',
7943
+ 'status.contextEstimateHistory': 'Current retained history estimate; not provider billing.',
7944
+ 'status.contextEstimateRequest': 'Submitted request estimate, including system and tool schemas; not provider billing.',
7945
+ 'status.contextEstimateImages': 'Image content is present; image token cost is not included in the text estimate.',
7631
7946
  'status.contextNearLimit': 'Context near limit',
7632
7947
  'status.contextOverLimit': 'Context over limit',
7633
7948
  'status.contextModeFallback': 'fallback',
@@ -7636,12 +7951,12 @@ var NEWMARK_I18N = {
7636
7951
  'status.noCompression': 'No compression event in this conversation',
7637
7952
  'status.contextInspector': 'Context management',
7638
7953
  'status.contextInspectorHint': 'Live context budget; visible chat history stays unchanged.',
7639
- 'status.activeBuild': 'Active Build',
7640
- 'status.longHistory': 'Long history',
7954
+ 'status.activeBuild': 'Build history (estimated)',
7955
+ 'status.longHistory': 'Long history (estimated)',
7641
7956
  'status.trigger': 'Trigger',
7642
7957
  'status.retention': 'Retention',
7643
- 'status.hotCache': 'Hot cache',
7644
- 'status.coldArchive': 'Cold archive',
7958
+ 'status.hotCache': 'Local history cache',
7959
+ 'status.coldArchive': 'Local history archive',
7645
7960
  'status.lastCompression': 'Last compression',
7646
7961
  'status.noCompressionShort': 'No compression yet',
7647
7962
  'status.compressNow': 'Compress now',
@@ -8358,9 +8673,17 @@ var NEWMARK_I18N = {
8358
8673
  'status.subagentRecords': '个活跃或保留的 agent 记录',
8359
8674
  'status.contextCompressed': '上下文已压缩',
8360
8675
  'status.contextCompression': '上下文压缩',
8361
- 'status.contextTokens': '上下文 token',
8362
- 'status.conversationTokens': '全对话 token',
8363
- 'status.cacheHitRate': '缓存命中率',
8676
+ 'status.contextTokens': '上下文 token(估算)',
8677
+ 'status.conversationTokens': '对话累计用量(实测)',
8678
+ 'status.cacheHitRate': '对话总缓存命中率',
8679
+ 'status.usageUnavailable': '未上报',
8680
+ 'status.cacheReportedPortion': '已上报部分',
8681
+ 'status.lastReportedInput': '最近主请求输入(实测)',
8682
+ 'status.requestOverhead': '系统 / 工具(估算)',
8683
+ 'status.requestOverheadExcluded': '当前历史估算未计入',
8684
+ 'status.contextEstimateHistory': '当前保留历史的本地估算,不代表服务端计费用量。',
8685
+ 'status.contextEstimateRequest': '已提交请求的本地估算,含系统提示与工具 schema,不代表服务端计费用量。',
8686
+ 'status.contextEstimateImages': '请求包含图片;文本估算未计入图片 token 开销。',
8364
8687
  'status.contextNearLimit': '上下文接近上限',
8365
8688
  'status.contextOverLimit': '上下文已超限',
8366
8689
  'status.contextModeFallback': 'fallback',
@@ -8369,12 +8692,12 @@ var NEWMARK_I18N = {
8369
8692
  'status.noCompression': '当前对话暂无压缩事件',
8370
8693
  'status.contextInspector': '上下文管理',
8371
8694
  'status.contextInspectorHint': '实时上下文预算;可见对话历史不会被改写。',
8372
- 'status.activeBuild': '当前 Build',
8373
- 'status.longHistory': '长期历史',
8695
+ 'status.activeBuild': 'Build 内历史(估算)',
8696
+ 'status.longHistory': '长期历史(估算)',
8374
8697
  'status.trigger': '触发线',
8375
8698
  'status.retention': '保留量',
8376
- 'status.hotCache': '热缓存',
8377
- 'status.coldArchive': '冷归档',
8699
+ 'status.hotCache': '本地历史缓存',
8700
+ 'status.coldArchive': '本地历史归档',
8378
8701
  'status.lastCompression': '最近一次压缩',
8379
8702
  'status.noCompressionShort': '尚未压缩',
8380
8703
  'status.compressNow': '立即压缩',
@@ -9000,22 +9323,18 @@ function syncTerminalShellOptions() {
9000
9323
 
9001
9324
  // === API Bridge ===
9002
9325
  var api = window.api || {
9003
- queueAction: async function(action, input) {
9326
+ queueAction: async function(action, input, requestedTarget) {
9004
9327
  try {
9005
- var target = normalizeQueuedConversationTarget(currentConversationTarget());
9328
+ var target = normalizeQueuedConversationTarget(requestedTarget || currentConversationTarget());
9006
9329
  if (!target) return { ok: false, error: 'No active conversation target' };
9007
9330
  var body = {
9008
9331
  workspaceId: target.workspaceId,
9009
9332
  conversationId: target.conversationId,
9010
- action: action,
9011
- id: String(input && input.id || ''),
9012
- text: String(input && input.text || ''),
9013
- value: String(input && input.text || ''),
9014
- requestedMode: String(input && input.requestedMode || 'build'),
9015
- goalObjective: String(input && input.goalObjective || ''),
9016
- createdAt: String(input && input.createdAt || ''),
9333
+ action: action
9017
9334
  };
9018
- if (Array.isArray(input && input.orderedIds)) body.orderedIds = input.orderedIds;
9335
+ ['id', 'text', 'value', 'requestedMode', 'goalObjective', 'createdAt', 'images', 'paused', 'orderedIds'].forEach(function(field) {
9336
+ if (input && Object.prototype.hasOwnProperty.call(input, field)) body[field] = input[field];
9337
+ });
9019
9338
  var r = await fetch('/api/queue-action', {
9020
9339
  method: 'POST',
9021
9340
  headers: { 'Content-Type': 'application/json' },
@@ -9024,11 +9343,20 @@ var api = window.api || {
9024
9343
  return r.json();
9025
9344
  } catch(e) { return { ok: false, error: e.message }; }
9026
9345
  },
9027
- sendMessage: async function(msg, conversationId) {
9346
+ sendMessage: async function(msg, requestedTarget, options) {
9028
9347
  try {
9029
- var body = {message: msg, mode: state.mode, model: state.model, intelligence: state.intelligence, inputMode: state.inputMode};
9030
- if (conversationId) body.conversation = conversationId;
9031
- if (state.mode === 'flow' && state.defaultFlow) body.defaultFlow = state.defaultFlow;
9348
+ options = options || {};
9349
+ var target = normalizeQueuedConversationTarget(typeof requestedTarget === 'object' ? requestedTarget : currentConversationTarget(requestedTarget));
9350
+ var body = {message: msg, mode: options.requestedMode || state.mode, model: state.model, intelligence: state.intelligence, inputMode: options.inputMode || state.inputMode};
9351
+ if (target) { body.conversation = target.conversationId; body.conversationId = target.conversationId; body.workspaceId = target.workspaceId; }
9352
+ body.requestedMode = options.requestedMode || state.mode;
9353
+ body.goalObjective = options.goalObjective || '';
9354
+ body.clientMessageId = options.clientMessageId || '';
9355
+ if (body.mode === 'flow') {
9356
+ body.flowName = options.flowName || state.defaultFlow;
9357
+ body.defaultFlow = body.flowName;
9358
+ }
9359
+ if (options.flowStart !== undefined) body.flowStart = options.flowStart;
9032
9360
  var r = await fetch('/api/send', {
9033
9361
  method: 'POST',
9034
9362
  headers: {'Content-Type': 'application/json'},
@@ -9037,8 +9365,13 @@ var api = window.api || {
9037
9365
  return r.json();
9038
9366
  } catch(e) { return {error: e.message}; }
9039
9367
  },
9040
- getState: async function() {
9041
- try { var r = await fetch('/api/state'); return r.json(); }
9368
+ getState: async function(requestedTarget) {
9369
+ try {
9370
+ var target = normalizeQueuedConversationTarget(typeof requestedTarget === 'object' ? requestedTarget : currentConversationTarget(requestedTarget));
9371
+ var query = target ? '?' + new URLSearchParams({ workspaceId: target.workspaceId, conversationId: target.conversationId }).toString() : '';
9372
+ var r = await fetch('/api/state' + query);
9373
+ return r.json();
9374
+ }
9042
9375
  catch(e) { return {}; }
9043
9376
  },
9044
9377
  getConversationDraft: async function(conversationId) {
@@ -9048,8 +9381,19 @@ var api = window.api || {
9048
9381
  saveConversationDraft: async function(conversationId, draft) {
9049
9382
  try { await fetch('/api/conversation-draft', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({conversationId:conversationId || '', draft:draft === null || draft === undefined ? '' : String(draft)})}); } catch(e) {}
9050
9383
  },
9051
- setMode: async function(m) {
9052
- try { await fetch('/api/mode', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:m})}); } catch(e) {}
9384
+ setMode: async function(m, requestedTarget) {
9385
+ try {
9386
+ var target = normalizeQueuedConversationTarget(requestedTarget || currentConversationTarget());
9387
+ var r = await fetch('/api/mode', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(Object.assign({mode:m}, target || {}))});
9388
+ return r.json();
9389
+ } catch(e) { return { ok: false, error: e.message }; }
9390
+ },
9391
+ setInputMode: async function(mode, requestedTarget) {
9392
+ try {
9393
+ var target = normalizeQueuedConversationTarget(requestedTarget || currentConversationTarget());
9394
+ var r = await fetch('/api/input-mode', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(Object.assign({inputMode: mode}, target || {}))});
9395
+ return r.json();
9396
+ } catch(e) { return { ok: false, error: e.message }; }
9053
9397
  },
9054
9398
  setModel: async function(m) {
9055
9399
  try { await fetch('/api/model', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:m})}); } catch(e) {}
@@ -10293,7 +10637,7 @@ function registerRuntimeKey(target, runtimeKey) {
10293
10637
  if (!state.runtimeKeyAliases) state.runtimeKeyAliases = {};
10294
10638
  state.runtimeKeyAliases[base] = runtimeKey;
10295
10639
  if (previous !== runtimeKey) {
10296
- ['runningConversations', 'conversationRuntimeStates', 'agentWorkEventsByConversation', 'agentWorkUiByConversation', 'workRunsByTarget', 'workRunsByBranch', 'runtimeBranchPathsByTarget', 'guideMessagesByTarget', 'trackedConversationUntil', 'backendQueuesByTarget', 'activeSendCallsByTarget', 'nextQueueSchedulesByTarget', 'nextQueueDrainsByTarget', 'queuePausedByTarget', 'conversationDrafts'].forEach(function(storeName) {
10640
+ ['runningConversations', 'conversationRuntimeStates', 'conversationWorkRevisions', 'agentWorkEventsByConversation', 'agentWorkUiByConversation', 'workRunsByTarget', 'workRunsByBranch', 'runtimeBranchPathsByTarget', 'guideMessagesByTarget', 'trackedConversationUntil', 'backendQueuesByTarget', 'activeSendCallsByTarget', 'nextQueueSchedulesByTarget', 'nextQueueDrainsByTarget', 'queuePausedByTarget', 'conversationDrafts'].forEach(function(storeName) {
10297
10641
  var store = state[storeName];
10298
10642
  if (!store || store[previous] === undefined) return;
10299
10643
  if (store[runtimeKey] === undefined) store[runtimeKey] = store[previous];
@@ -10535,12 +10879,14 @@ function setConversationRuntimeState(target, status, runId, extra) {
10535
10879
  target = target || currentConversationTarget();
10536
10880
  var key = runtimeKeyFor(target.workspaceId, target.conversationId);
10537
10881
  if (!state.conversationRuntimeStates) state.conversationRuntimeStates = {};
10882
+ var previousRuntime = state.conversationRuntimeStates[key];
10538
10883
  var next = Object.assign({}, state.conversationRuntimeStates[key] || {}, extra || {}, {
10539
10884
  target: { workspaceId: String(target.workspaceId || ''), conversationId: String(target.conversationId || 'default') },
10540
10885
  status: String(status || 'idle'),
10541
10886
  runId: String(runId || (state.conversationRuntimeStates[key] && state.conversationRuntimeStates[key].runId) || '')
10542
10887
  });
10543
10888
  state.conversationRuntimeStates[key] = next;
10889
+ if (!previousRuntime || previousRuntime.runId !== next.runId || previousRuntime.status !== next.status) advanceConversationWorkRevision(target);
10544
10890
  if (!state.runningConversations) state.runningConversations = {};
10545
10891
  if (['running', 'stopping', 'force_restarting'].indexOf(next.status) >= 0) state.runningConversations[key] = next;
10546
10892
  else delete state.runningConversations[key];
@@ -10595,6 +10941,9 @@ function requestBackendStopForProvisionalStart(target, runId) {
10595
10941
  ? api.stopConversation({ target: target, runId: String(runId || ''), force: false })
10596
10942
  : (api.abortConversation ? api.abortConversation(target) : Promise.resolve({ action: 'not_running' }));
10597
10943
  Promise.resolve(stopPromise).then(function(result) {
10944
+ if (!conversationRunStillOwnsTarget(target, runId)) return;
10945
+ var currentStop = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(target.workspaceId, target.conversationId)];
10946
+ if (currentStop && currentStop.status !== 'stopping') return;
10598
10947
  var action = String(result && result.action || '');
10599
10948
  if (action === 'not_running' || action === 'stale') {
10600
10949
  return refreshConversationRuntimeAfterStopRace(target, runId, result);
@@ -10611,6 +10960,9 @@ function requestBackendStopForProvisionalStart(target, runId) {
10611
10960
  setConversationRuntimeState(target, 'interrupted', runId, { provisional: false });
10612
10961
  }
10613
10962
  }).catch(function(error) {
10963
+ if (!conversationRunStillOwnsTarget(target, runId)) return;
10964
+ var currentStop = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(target.workspaceId, target.conversationId)];
10965
+ if (currentStop && currentStop.status !== 'stopping') return;
10614
10966
  setConversationRuntimeState(target, 'running', runId, { provisional: false });
10615
10967
  showUiNotice(error && error.message ? error.message : String(error), 'error', 'stop-provisional-' + runtimeKeyFor(target.workspaceId, target.conversationId) + '-' + String(runId || ''));
10616
10968
  });
@@ -10728,6 +11080,7 @@ function publicWorkEventForUi(event) {
10728
11080
  model: String(event.model || ''),
10729
11081
  timestamp: String(event.timestamp || ''),
10730
11082
  toolName: toolName,
11083
+ toolCallId: event.toolCallId,
10731
11084
  toolArgs: type === 'tool_call' ? String(event.toolArgs || '') : '',
10732
11085
  workspaceId: String(event.workspaceId || (event.target && event.target.workspaceId) || ''),
10733
11086
  workspaceKey: event.workspaceKey,
@@ -10916,9 +11269,10 @@ function syncWorkRunsSnapshot(runs, target, branchId) {
10916
11269
  if (liveOnlyEvents.length) {
10917
11270
  item.events = dedupeGuideWorkEvents(item.events.concat(liveOnlyEvents)).sort(compareConversationWorkEvents);
10918
11271
  item.sequence = Math.max(Number(item.sequence || 0), Number(previous.sequence || 0));
10919
- if (['running', 'stopping', 'force_restarting'].indexOf(String(previous.status || '')) >= 0) {
11272
+ if (Number(previous.sequence || 0) > Number(run.sequence || 0)) {
10920
11273
  item.status = previous.status;
10921
11274
  item.endedAt = previous.endedAt;
11275
+ item.expanded = previous.expanded;
10922
11276
  }
10923
11277
  }
10924
11278
  }
@@ -11473,6 +11827,31 @@ function renderWorkRunEvents(run, includeGuides) {
11473
11827
  }).join('') + '</div>';
11474
11828
  }
11475
11829
 
11830
+ // A length/ID key alone cannot detect an equal-length correction or the
11831
+ // transition from large live plain text to terminal Markdown. Keep the exact
11832
+ // render input on the DOM node, not in a large data-* attribute or global cache.
11833
+ function workRunEventRenderState(event, run, terminalInterrupted) {
11834
+ var type = String(event && event.type || '').toLowerCase();
11835
+ if (['public_text', 'partial_text', 'response', 'thought'].indexOf(type) < 0) return null;
11836
+ var live = ['running', 'stopping', 'force_restarting'].indexOf(String(run && run.status || '').toLowerCase()) >= 0;
11837
+ return currentLang() + ':' + (live ? 'live' : 'terminal') + ':' + (terminalInterrupted ? 'partial' : 'whole') + ':' + String(event && event.content || '');
11838
+ }
11839
+
11840
+ function rememberWorkRunEventRenderStates(run, body, expanded) {
11841
+ var container = body && body.querySelector('.conversation-work-run-events');
11842
+ if (!container) return;
11843
+ var presented = presentedWorkRunEvents(run, expanded);
11844
+ Array.prototype.forEach.call(container.children, function(node, index) {
11845
+ node._newmarkWorkEventRenderState = workRunEventRenderState(presented.events[index], run, presented.terminalInterrupted);
11846
+ });
11847
+ }
11848
+
11849
+ function setWorkRunStackHtml(stack, html) {
11850
+ if (stack._newmarkWorkStackHtml === html) return;
11851
+ stack.innerHTML = html;
11852
+ stack._newmarkWorkStackHtml = html;
11853
+ }
11854
+
11476
11855
  // 增量更新 run body 内的事件列表:保留与新呈现列表 key 一致的前缀节点,
11477
11856
  // 从第一个分歧节点起重建尾部。运行中通常只有最后一个文本/thought 节点
11478
11857
  // 增长或尾部追加,因此不会整块重绘整个进程渲染。
@@ -11481,6 +11860,7 @@ function updateWorkRunEventsContainer(run, body, expanded) {
11481
11860
  var container = body.querySelector ? body.querySelector('.conversation-work-run-events') : null;
11482
11861
  if (!container) {
11483
11862
  body.innerHTML = renderWorkRunEvents(run, expanded);
11863
+ rememberWorkRunEventRenderStates(run, body, expanded);
11484
11864
  return;
11485
11865
  }
11486
11866
  if (!presented.events.length) {
@@ -11491,7 +11871,8 @@ function updateWorkRunEventsContainer(run, body, expanded) {
11491
11871
  var nodes = Array.prototype.slice.call(container.children);
11492
11872
  var keep = 0;
11493
11873
  while (keep < nodes.length && keep < presented.events.length) {
11494
- if (String(nodes[keep].getAttribute('data-work-event-key') || '') === workRunPresentedEventKey(presented.events[keep], keep)) keep++;
11874
+ if (String(nodes[keep].getAttribute('data-work-event-key') || '') === workRunPresentedEventKey(presented.events[keep], keep)
11875
+ && nodes[keep]._newmarkWorkEventRenderState === workRunEventRenderState(presented.events[keep], run, presented.terminalInterrupted)) keep++;
11495
11876
  else break;
11496
11877
  }
11497
11878
  while (container.children.length > keep) container.removeChild(container.lastChild);
@@ -11502,6 +11883,7 @@ function updateWorkRunEventsContainer(run, body, expanded) {
11502
11883
  while (holder.firstChild) {
11503
11884
  var node = holder.firstChild;
11504
11885
  node.setAttribute('data-work-event-key', key);
11886
+ node._newmarkWorkEventRenderState = workRunEventRenderState(presented.events[i], run, presented.terminalInterrupted);
11505
11887
  container.appendChild(node);
11506
11888
  }
11507
11889
  }
@@ -11546,6 +11928,7 @@ function updateConversationWorkRunElement(run, element) {
11546
11928
  '<span class="conversation-work-run-status">' + esc(String(run.status || '')) + '</span>' +
11547
11929
  '<span class="conversation-work-run-chevron" aria-hidden="true"></span></button>' +
11548
11930
  '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>';
11931
+ rememberWorkRunEventRenderStates(run, element.querySelector('.conversation-work-run-body'), expanded);
11549
11932
  } else {
11550
11933
  // 就地更新头部,不重建 head 按钮(保住聚焦/动画状态)。
11551
11934
  var titleEl = head.querySelector('.conversation-work-run-title');
@@ -11581,7 +11964,10 @@ function updateConversationWorkRunElement(run, element) {
11581
11964
  }
11582
11965
  var body = element.querySelector('.conversation-work-run-body');
11583
11966
  if (body) updateWorkRunEventsContainer(run, body, expanded);
11584
- else element.insertAdjacentHTML('beforeend', '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>');
11967
+ else {
11968
+ element.insertAdjacentHTML('beforeend', '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>');
11969
+ rememberWorkRunEventRenderStates(run, element.querySelector('.conversation-work-run-body'), expanded);
11970
+ }
11585
11971
  }
11586
11972
  renderPendingGuideMessages(run.target, {});
11587
11973
  var wrapper = element.closest ? element.closest('.work-run-message') : null;
@@ -11594,14 +11980,14 @@ function updateConversationWorkRunElement(run, element) {
11594
11980
  imageStack.className = 'work-run-collapsed-images';
11595
11981
  }
11596
11982
  if (imageStack.previousElementSibling !== element) wrapper.insertBefore(imageStack, element.nextSibling);
11597
- imageStack.innerHTML = expanded ? '' : displayImages.map(renderWorkDisplayImage).join('');
11983
+ setWorkRunStackHtml(imageStack, expanded ? '' : displayImages.map(renderWorkDisplayImage).join(''));
11598
11984
  var guideStack = wrapper.querySelector(':scope > .work-run-collapsed-guides');
11599
11985
  if (!guideStack) {
11600
11986
  guideStack = document.createElement('div');
11601
11987
  guideStack.className = 'work-run-collapsed-guides';
11602
11988
  wrapper.appendChild(guideStack);
11603
11989
  }
11604
- guideStack.innerHTML = expanded ? '' : workRunGuideEvents(run).map(renderWorkRunGuideMessage).join('');
11990
+ setWorkRunStackHtml(guideStack, expanded ? '' : workRunGuideEvents(run).map(renderWorkRunGuideMessage).join(''));
11605
11991
  }
11606
11992
  hydrateWorkDisplayImages(wrapper || element, (run.events || []).map(function(event) { return event && event.displayImage; }).filter(Boolean));
11607
11993
  hydrateWorkRunGuideMessages(wrapper || element, run);
@@ -12085,7 +12471,10 @@ function renderChatMessages(messages, target) {
12085
12471
  var liveArea = els['chat-area'];
12086
12472
  if (!liveArea) return;
12087
12473
  var previousRendered = Array.isArray(state.renderedChatMessages) ? state.renderedChatMessages.slice() : [];
12088
- var previousHtml = liveArea.innerHTML;
12474
+ // Staging leaves the live transcript untouched until commit. Keep its nodes
12475
+ // for rollback instead of serializing a potentially long HTML transcript;
12476
+ // this also preserves listeners if rendering fails.
12477
+ var previousNodes = Array.prototype.slice.call(liveArea.childNodes);
12089
12478
  var previousScrollTop = liveArea.scrollTop;
12090
12479
  var wasAtBottom = shouldAutoScroll(liveArea);
12091
12480
  var stagingArea = liveArea.cloneNode(false);
@@ -12097,7 +12486,7 @@ function renderChatMessages(messages, target) {
12097
12486
  else liveArea.scrollTop = previousScrollTop;
12098
12487
  } catch (error) {
12099
12488
  state.renderedChatMessages = previousRendered;
12100
- liveArea.innerHTML = previousHtml;
12489
+ liveArea.replaceChildren.apply(liveArea, previousNodes);
12101
12490
  liveArea.scrollTop = previousScrollTop;
12102
12491
  console.error('[ui_chat_render_failed]', error);
12103
12492
  showUiNotice(currentLang() === 'zh' ? '对话渲染出现异常,已保留原内容。' : 'Chat rendering failed; the previous content was preserved.', 'error', 'chat-render-failed');
@@ -12278,20 +12667,25 @@ function renderLoadEarlierButton() {
12278
12667
  button.className = 'conversation-load-earlier';
12279
12668
  button.textContent = currentLang() === 'zh' ? ('加载更早的 ' + Number(before) + ' 条消息') : ('Load ' + Number(before) + ' earlier messages');
12280
12669
  button.addEventListener('click', function() {
12281
- var loadTarget = currentConversationTarget();
12670
+ var loadTarget = target;
12671
+ if (!isActiveConversationTarget(loadTarget) || button.disabled) return;
12282
12672
  var loadKey = runtimeKeyFor(loadTarget.workspaceId, loadTarget.conversationId);
12283
12673
  var loadBefore = state.conversationLoadedBefore && state.conversationLoadedBefore[loadKey];
12284
12674
  if (!Number.isFinite(Number(loadBefore)) || Number(loadBefore) <= 0) return;
12675
+ var loadGeneration = state.conversationLoadGeneration;
12676
+ var loadBranch = JSON.stringify([state.activeConversationBranchId || '', state.viewedConversationBranchNodePath || []]);
12285
12677
  button.disabled = true;
12286
12678
  api.loadEarlierConversationMessages({ workspaceId: loadTarget.workspaceId, conversationId: loadTarget.conversationId, window: 200, before: loadBefore }).then(function(result) {
12287
12679
  button.disabled = false;
12680
+ if (!isActiveConversationTarget(loadTarget) || loadGeneration !== state.conversationLoadGeneration
12681
+ || loadBranch !== JSON.stringify([state.activeConversationBranchId || '', state.viewedConversationBranchNodePath || []])
12682
+ || Number(state.conversationLoadedBefore[loadKey]) !== Number(loadBefore)) return;
12288
12683
  if (!result || !Array.isArray(result.chatMessages) || !result.chatMessages.length) return;
12289
12684
  var newer = state.renderedChatMessages || [];
12290
- state.renderedChatMessages = (result.chatMessages || []).concat(newer);
12685
+ var mergedMessages = result.chatMessages.concat(newer);
12291
12686
  state.conversationLoadedBefore[loadKey] = Math.max(0, Number(result.windowStart) || 0);
12292
12687
  var chatWasAtBottom = shouldAutoScroll(els['chat-area']);
12293
- els['chat-area'].innerHTML = '';
12294
- renderChatMessages(state.renderedChatMessages);
12688
+ renderChatMessages(mergedMessages, loadTarget);
12295
12689
  if (!chatWasAtBottom && els['chat-area']) els['chat-area'].scrollTop = 0;
12296
12690
  renderLoadEarlierButton();
12297
12691
  }).catch(function() { button.disabled = false; });
@@ -12553,29 +12947,88 @@ function replayActiveAgentWorkEvents() {
12553
12947
  }
12554
12948
 
12555
12949
  var _conversationsRenderPending = false;
12950
+ function conversationWorkRevision(target) {
12951
+ var key = runtimeKeyFor(target.workspaceId, target.conversationId);
12952
+ return Number(state.conversationWorkRevisions && state.conversationWorkRevisions[key] || 0);
12953
+ }
12954
+ function advanceConversationWorkRevision(target) {
12955
+ if (!state.conversationWorkRevisions) state.conversationWorkRevisions = {};
12956
+ state.conversationWorkRevisions[runtimeKeyFor(target.workspaceId, target.conversationId)] = conversationWorkRevision(target) + 1;
12957
+ }
12958
+ function captureConversationWorkSnapshotRequest(target) {
12959
+ return { target: { workspaceId: target.workspaceId, conversationId: target.conversationId },
12960
+ loadGeneration: state.conversationLoadGeneration, revision: conversationWorkRevision(target) };
12961
+ }
12962
+ function isCurrentConversationWorkSnapshotRequest(request) {
12963
+ return !!request && isActiveConversationTarget(request.target)
12964
+ && request.loadGeneration === state.conversationLoadGeneration
12965
+ && request.revision === conversationWorkRevision(request.target);
12966
+ }
12967
+ function conversationRunStillOwnsTarget(target, runId, alternateRunId) {
12968
+ var latest = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(target.workspaceId, target.conversationId)];
12969
+ return !latest || !latest.runId || String(latest.runId) === String(runId || '')
12970
+ || (!!alternateRunId && String(latest.runId) === String(alternateRunId));
12971
+ }
12556
12972
  var _contextWindowRefreshTimer = null;
12557
12973
  var _contextWindowRefreshInFlight = false;
12558
12974
  var _contextWindowRefreshTargetKey = '';
12975
+ var _contextWindowRefreshPendingTarget = null;
12976
+ var _contextWindowRefreshRevision = 0;
12977
+ var _contextWindowSnapshotRequestId = 0;
12978
+
12979
+ function captureContextWindowSnapshotRequest(target) {
12980
+ return {
12981
+ target: { workspaceId: target.workspaceId, conversationId: target.conversationId },
12982
+ loadGeneration: state.conversationLoadGeneration,
12983
+ revision: _contextWindowRefreshRevision,
12984
+ requestId: isActiveConversationTarget(target) ? ++_contextWindowSnapshotRequestId : 0
12985
+ };
12986
+ }
12987
+
12988
+ function isCurrentContextWindowSnapshotRequest(request) {
12989
+ return !!request && isActiveConversationTarget(request.target)
12990
+ && request.loadGeneration === state.conversationLoadGeneration
12991
+ && request.revision === _contextWindowRefreshRevision
12992
+ && request.requestId === _contextWindowSnapshotRequestId;
12993
+ }
12994
+
12995
+ function applyContextWindowSnapshot(snapshot, request) {
12996
+ if (!snapshot || !isCurrentContextWindowSnapshotRequest(request)) return false;
12997
+ if (snapshot.contextCompression !== undefined) state.contextCompression = snapshot.contextCompression;
12998
+ if (snapshot.contextWindow !== undefined) state.contextWindow = snapshot.contextWindow;
12999
+ if (snapshot.contextWindow !== undefined || snapshot.contextCompression !== undefined) window.renderContextWindow();
13000
+ return true;
13001
+ }
12559
13002
 
12560
13003
  function scheduleActiveContextWindowRefresh(target) {
12561
13004
  if (!api.getState || !target || !isActiveConversationTarget(target)) return;
12562
13005
  var targetKey = runtimeKeyFor(target.workspaceId, target.conversationId);
12563
13006
  _contextWindowRefreshTargetKey = targetKey;
12564
- if (_contextWindowRefreshTimer !== null) return;
13007
+ _contextWindowRefreshPendingTarget = { workspaceId: target.workspaceId, conversationId: target.conversationId };
13008
+ _contextWindowRefreshRevision += 1;
13009
+ if (_contextWindowRefreshTimer !== null || _contextWindowRefreshInFlight) return;
12565
13010
  _contextWindowRefreshTimer = window.setTimeout(function() {
12566
13011
  _contextWindowRefreshTimer = null;
12567
- if (_contextWindowRefreshInFlight || !isActiveConversationTarget(target)) return;
13012
+ var requestTarget = _contextWindowRefreshPendingTarget;
13013
+ _contextWindowRefreshPendingTarget = null;
13014
+ if (!requestTarget || !isActiveConversationTarget(requestTarget)) return;
13015
+ var requestKey = runtimeKeyFor(requestTarget.workspaceId, requestTarget.conversationId);
13016
+ var requestRevision = _contextWindowRefreshRevision;
13017
+ var loadGeneration = state.conversationLoadGeneration;
13018
+ var contextRequest = captureContextWindowSnapshotRequest(requestTarget);
12568
13019
  _contextWindowRefreshInFlight = true;
12569
- api.getState(target).then(function(snapshot) {
12570
- if (!snapshot || !isActiveConversationTarget(target) || _contextWindowRefreshTargetKey !== targetKey) return;
12571
- if (snapshot.contextWindow !== undefined) {
12572
- state.contextWindow = snapshot.contextWindow;
12573
- window.renderContextWindow();
12574
- }
12575
- if (snapshot.contextCompression !== undefined) state.contextCompression = snapshot.contextCompression;
13020
+ api.getState(requestTarget).then(function(snapshot) {
13021
+ if (!snapshot || !isActiveConversationTarget(requestTarget) || _contextWindowRefreshTargetKey !== requestKey
13022
+ || requestRevision !== _contextWindowRefreshRevision || loadGeneration !== state.conversationLoadGeneration) return;
13023
+ applyContextWindowSnapshot(snapshot, contextRequest);
12576
13024
  if (state.rightTab === 'status') window.renderRightStatusPanel();
12577
13025
  }).catch(function(){}).then(function() {
12578
13026
  _contextWindowRefreshInFlight = false;
13027
+ // A semantic event during IPC still requires one fresh snapshot. Keep
13028
+ // only the latest target, with no polling once the event stream is quiet.
13029
+ var pendingTarget = _contextWindowRefreshPendingTarget;
13030
+ _contextWindowRefreshPendingTarget = null;
13031
+ if (pendingTarget) scheduleActiveContextWindowRefresh(pendingTarget);
12579
13032
  });
12580
13033
  }, 120);
12581
13034
  }
@@ -12589,8 +13042,49 @@ function scheduleConversationsRender() {
12589
13042
  });
12590
13043
  }
12591
13044
 
13045
+ function applyWorkspaceConversationList(event) {
13046
+ if (event.stateScope !== 'workspace' || !String(event.workspaceId || '').trim() || !Array.isArray(event.conversations)) return;
13047
+ var workspaceId = String(event.workspaceId).trim();
13048
+ var key = currentWorkspaceKey(workspaceId);
13049
+ var foreground = key === currentWorkspaceKey();
13050
+ var rows = (state.workspaceConversations && state.workspaceConversations[key]) || [];
13051
+ var selectedIndex = state.workspaceActiveConversation && state.workspaceActiveConversation[key];
13052
+ var selected = rows[selectedIndex] || rows.find(function(row) { return row.active; });
13053
+ var selectedId = foreground ? activeConversationId() : String(selected && selected.id || '');
13054
+ // A remote create/rename/delete changes this workspace's list, never the
13055
+ // desktop user's selected conversation or the execution target.
13056
+ applyBackendConversations(event.conversations, selectedId, workspaceId);
13057
+ if (foreground) scheduleConversationsRender();
13058
+ }
13059
+
13060
+ function expandedWorkEventDeltas(event) {
13061
+ if (event && (event.type === 'text' || event.type === 'thought_delta') && Array.isArray(event.coalescedDeltas) && event.coalescedDeltas.length) {
13062
+ return event.coalescedDeltas.map(function(delta) { return Object.assign({}, event, delta, { coalescedDeltas: undefined }); });
13063
+ }
13064
+ return [event];
13065
+ }
13066
+
13067
+ function mergeConversationWorkEventSnapshots(existing, snapshot) {
13068
+ var seen = {};
13069
+ var result = [];
13070
+ existing.concat(snapshot).forEach(function(event) {
13071
+ expandedWorkEventDeltas(event).forEach(function(item) {
13072
+ if (!item) return;
13073
+ var id = String(item.id || [item.timestamp || '', item.type || '', item.content || '', item.toolName || '', item.toolArgs || ''].join('|'));
13074
+ if (seen[id]) return;
13075
+ seen[id] = true;
13076
+ result.push(item);
13077
+ });
13078
+ });
13079
+ return result;
13080
+ }
13081
+
12592
13082
  function appendAgentWorkEvent(event) {
12593
13083
  if (!event) return;
13084
+ if ((event.type === 'text' || event.type === 'thought_delta') && Array.isArray(event.coalescedDeltas) && event.coalescedDeltas.length) {
13085
+ expandedWorkEventDeltas(event).forEach(appendAgentWorkEvent);
13086
+ return;
13087
+ }
12594
13088
  // Subagent (peer) work events are streamed to their own live history overlay
12595
13089
  // and never touch the main conversation's chat/runtime state.
12596
13090
  if (String(event.actorId || '')) {
@@ -12613,12 +13107,24 @@ function appendAgentWorkEvent(event) {
12613
13107
  }
12614
13108
  return;
12615
13109
  }
13110
+ if (String(event.type || '') === 'conversation_list') {
13111
+ applyWorkspaceConversationList(event);
13112
+ return;
13113
+ }
12616
13114
  event = publicWorkEventForUi(event);
12617
13115
  var id = String(event.conversationId || (event.target && event.target.conversationId) || 'default');
12618
13116
  var workspaceId = String(event.workspaceId || (event.target && event.target.workspaceId) || runtimeWorkspaceId(''));
12619
13117
  var target = { workspaceId: workspaceId, conversationId: id };
12620
13118
  eventRuntimeKey(event);
13119
+ if (event.runId) advanceConversationWorkRevision(target);
12621
13120
  var active = isActiveConversationTarget(target);
13121
+ if (event.type === 'queue_update') {
13122
+ // Queue ownership is independent of a renderer send promise and of the
13123
+ // history branch being viewed. Remote edits must refresh every target.
13124
+ applyConversationCommandSnapshot(event, target);
13125
+ if (active) window.renderInputStack();
13126
+ return;
13127
+ }
12622
13128
  var visibleRuntimeBranch = active && isViewingRuntimeConversationBranch(target);
12623
13129
  // 模型回退不是隐藏的参数回退:活跃对话发生回退时,把输入框下方的
12624
13130
  // 模型选择区同步为实际生效的模型。
@@ -12685,13 +13191,10 @@ function appendAgentWorkEvent(event) {
12685
13191
  }
12686
13192
  if (active && terminalEvent && api.getState) {
12687
13193
  var terminalWorkspaceKey = currentWorkspaceKey();
13194
+ var terminalContextRequest = captureContextWindowSnapshotRequest(target);
12688
13195
  api.getState(target).then(function(snapshot) {
12689
- if (!isActiveConversationTarget(target) || terminalWorkspaceKey !== currentWorkspaceKey()) return;
12690
- if (snapshot && snapshot.contextWindow !== undefined) {
12691
- state.contextWindow = snapshot.contextWindow;
12692
- window.renderContextWindow();
12693
- }
12694
- if (snapshot && snapshot.contextCompression !== undefined) state.contextCompression = snapshot.contextCompression;
13196
+ if (!isCurrentContextWindowSnapshotRequest(terminalContextRequest) || terminalWorkspaceKey !== currentWorkspaceKey()) return;
13197
+ applyContextWindowSnapshot(snapshot, terminalContextRequest);
12695
13198
  if (state.rightTab === 'status') window.renderRightStatusPanel();
12696
13199
  // Backend-owned Goal continuations have no renderer send promise to
12697
13200
  // return the final snapshot. Refresh on the terminal work event so a
@@ -12779,6 +13282,7 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
12779
13282
  && !snapshotHasReadableConversationHistory(s)
12780
13283
  && (!options.allowEmptyHistory || conversationHasReadableHistory(snapshotTarget)));
12781
13284
  if (s && s.runtimeKey) registerRuntimeKey(snapshotTarget, s.runtimeKey);
13285
+ hydrateConversationDraftSnapshot(s, snapshotTarget);
12782
13286
  hydrateConversationBranchState(s);
12783
13287
  rebindQueueToRuntimeBranch(snapshotTarget);
12784
13288
  if (s && Array.isArray(s.workRuns) && !preserveReadableHistory) {
@@ -12788,6 +13292,18 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
12788
13292
  syncWorkRunsSnapshot(s.workRuns, snapshotTarget, viewedBranchIdForSync || undefined);
12789
13293
  }
12790
13294
  applyAutoRouteRatingState(s);
13295
+ if (s && Number.isFinite(Number(s.totalMessages))) {
13296
+ if (!state.conversationLoadedBefore) state.conversationLoadedBefore = {};
13297
+ var windowKey = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
13298
+ state.conversationLoadedBefore[windowKey] = Math.max(0, Number(s.totalMessages) - (Array.isArray(s.chatMessages) ? s.chatMessages.length : 0));
13299
+ }
13300
+ if (s.contextCompression !== undefined) state.contextCompression = s.contextCompression;
13301
+ if (s.contextWindow !== undefined || s.contextCompression !== undefined) {
13302
+ _contextWindowRefreshRevision += 1;
13303
+ if (s.contextWindow !== undefined) state.contextWindow = s.contextWindow;
13304
+ window.renderContextWindow();
13305
+ }
13306
+ if (state.rightTab === 'status') window.renderRightStatusPanel();
12791
13307
  if (s && Array.isArray(s.chatMessages) && !preserveReadableHistory) {
12792
13308
  renderChatMessages(s.chatMessages, snapshotTarget);
12793
13309
  renderLoadEarlierButton();
@@ -12797,11 +13313,6 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
12797
13313
  applyBackendConversations(s.conversations || [], s.conversationId || requestedConversationId);
12798
13314
  }
12799
13315
  markConversationTracked(s && s.conversationId ? s.conversationId : requestedConversationId, state.conversationTrackMs || 300000, snapshotTarget.workspaceId);
12800
- if (s && Number.isFinite(Number(s.totalMessages))) {
12801
- if (!state.conversationLoadedBefore) state.conversationLoadedBefore = {};
12802
- var windowKey = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
12803
- state.conversationLoadedBefore[windowKey] = Math.max(0, Number(s.totalMessages) - (Array.isArray(s.chatMessages) ? s.chatMessages.length : 0));
12804
- }
12805
13316
  if (s && s.queued) {
12806
13317
  state.backendQueue = setBackendQueueForTarget(s.queued, snapshotTarget);
12807
13318
  if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, snapshotTarget);
@@ -12811,16 +13322,7 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
12811
13322
  var eventConversationId = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
12812
13323
  if (!state.agentWorkEventsByConversation) state.agentWorkEventsByConversation = {};
12813
13324
  var existingEvents = state.agentWorkEventsByConversation[eventConversationId] || [];
12814
- var mergedEvents = existingEvents.concat(s.workEvents || []);
12815
- var seenEvents = {};
12816
- var dedupedEvents = [];
12817
- for (var wi = 0; wi < mergedEvents.length; wi++) {
12818
- var ev = mergedEvents[wi] || {};
12819
- var key = ev.id || [ev.timestamp || '', ev.type || '', ev.content || '', ev.toolName || '', ev.toolArgs || ''].join('|');
12820
- if (seenEvents[key]) continue;
12821
- seenEvents[key] = true;
12822
- dedupedEvents.push(ev);
12823
- }
13325
+ var dedupedEvents = mergeConversationWorkEventSnapshots(existingEvents, s.workEvents || []);
12824
13326
  state.agentWorkEventsByConversation[eventConversationId] = dedupedEvents.slice(-Number(state.agentWorkEventLimit || 240));
12825
13327
  }
12826
13328
  if (s && s.conversationPlan) {
@@ -12940,7 +13442,7 @@ function setVisibleMode(mode) {
12940
13442
  }
12941
13443
  if (nextMode === 'flow') window.setInputMode('guide');
12942
13444
  if (api.setMode) {
12943
- return Promise.resolve(api.setMode(nextMode)).then(function(result) {
13445
+ return Promise.resolve(api.setMode(nextMode, currentConversationTarget())).then(function(result) {
12944
13446
  if (result != null) state._syncedMode = nextMode;
12945
13447
  }).catch(function(){});
12946
13448
  }
@@ -13083,8 +13585,24 @@ function conversationDraftParts(key) {
13083
13585
  };
13084
13586
  }
13085
13587
 
13086
- function saveActiveConversationDraft() {
13588
+ function hydrateConversationDraftSnapshot(snapshot, target) {
13589
+ if (!snapshot || snapshot.draft === undefined) return;
13590
+ var key = conversationDraftKey(target);
13591
+ // A locally edited or explicitly cleared draft is newer than a returning
13592
+ // snapshot. Only the first hydration may populate this conversation's slot.
13593
+ if (Object.prototype.hasOwnProperty.call(state.conversationDrafts, key)) return;
13594
+ state.conversationDrafts[key] = String(snapshot.draft || '');
13595
+ if (isActiveConversationTarget(target) && (!state._promptBoundKey || state._promptBoundKey === key)) {
13596
+ restoreConversationDraft(target);
13597
+ }
13598
+ }
13599
+
13600
+ function saveActiveConversationDraft(fromInput) {
13087
13601
  if (!els.prompt) return;
13602
+ var key = state._promptBoundKey || conversationDraftKey();
13603
+ // A blank, not-yet-hydrated composer is not an instruction to erase a stored
13604
+ // draft. A real input event (including deleting all text) is authoritative.
13605
+ if (!fromInput && !els.prompt.value && !Object.prototype.hasOwnProperty.call(state.conversationDrafts, key)) return;
13088
13606
  // The prompt content belongs to the conversation it was last restored for.
13089
13607
  // While a conversation switch is still resolving (sync not yet complete),
13090
13608
  // the bound key stays on the previous conversation so fast switching cannot
@@ -13097,17 +13615,21 @@ function saveActiveConversationDraft() {
13097
13615
  // window. The in-memory window is never dropped, so switching back stays instant.
13098
13616
  // Writes are debounced per key (100ms) so rapid conversation switching coalesces
13099
13617
  // into a single request per conversation.
13100
- function persistConversationDraftForKey(key) {
13618
+ function persistConversationDraftForKey(key, delayMs) {
13101
13619
  if (!api.saveConversationDraft) return;
13102
13620
  var parts = conversationDraftParts(key);
13103
13621
  if (parts.workspaceId && parts.workspaceId !== String(state.currentWorkspaceId || '')) return;
13622
+ if (!Object.prototype.hasOwnProperty.call(state.conversationDrafts, conversationDraftKey(parts))) return;
13104
13623
  if (!state._draftPersistTimers) state._draftPersistTimers = {};
13105
13624
  if (state._draftPersistTimers[key]) clearTimeout(state._draftPersistTimers[key]);
13106
13625
  state._draftPersistTimers[key] = setTimeout(function() {
13107
13626
  delete state._draftPersistTimers[key];
13108
- var draft = Object.prototype.hasOwnProperty.call(state.conversationDrafts, key) ? state.conversationDrafts[key] : '';
13627
+ if (parts.workspaceId && parts.workspaceId !== String(state.currentWorkspaceId || '')) return;
13628
+ var resolvedKey = conversationDraftKey(parts);
13629
+ if (!Object.prototype.hasOwnProperty.call(state.conversationDrafts, resolvedKey)) return;
13630
+ var draft = state.conversationDrafts[resolvedKey];
13109
13631
  api.saveConversationDraft(parts.conversationId, draft).catch(function(){});
13110
- }, 100);
13632
+ }, delayMs === undefined ? 100 : delayMs);
13111
13633
  }
13112
13634
 
13113
13635
  function restoreConversationDraft(target) {
@@ -13128,13 +13650,15 @@ function flushPendingConversationDraftPersists() {
13128
13650
  delete state._draftPersistTimers[key];
13129
13651
  var parts = conversationDraftParts(key);
13130
13652
  if (parts.workspaceId && parts.workspaceId !== String(state.currentWorkspaceId || '')) return;
13131
- var draft = Object.prototype.hasOwnProperty.call(state.conversationDrafts, key) ? state.conversationDrafts[key] : '';
13653
+ var resolvedKey = conversationDraftKey(parts);
13654
+ if (!Object.prototype.hasOwnProperty.call(state.conversationDrafts, resolvedKey)) return;
13655
+ var draft = state.conversationDrafts[resolvedKey];
13132
13656
  if (api.saveConversationDraft) api.saveConversationDraft(parts.conversationId, draft).catch(function(){});
13133
13657
  });
13134
13658
  }
13135
13659
  function clearActiveConversationDraft() {
13136
13660
  var key = conversationDraftKey();
13137
- delete state.conversationDrafts[key];
13661
+ state.conversationDrafts[key] = '';
13138
13662
  if (state._draftPersistTimers && state._draftPersistTimers[key]) {
13139
13663
  clearTimeout(state._draftPersistTimers[key]);
13140
13664
  delete state._draftPersistTimers[key];
@@ -13242,10 +13766,12 @@ async function refreshConversationRuntimeAfterStopRace(target, expectedRunId, st
13242
13766
  target = target || currentConversationTarget();
13243
13767
  expectedRunId = String(expectedRunId || '');
13244
13768
  var action = String(stopResult && stopResult.action || '');
13769
+ var stopRevision = conversationWorkRevision(target);
13245
13770
  var snapshot = null;
13246
13771
  if (api.getState) {
13247
13772
  try { snapshot = await api.getState(target); } catch (_) { snapshot = null; }
13248
13773
  }
13774
+ if (!conversationRunStillOwnsTarget(target, expectedRunId) || stopRevision !== conversationWorkRevision(target)) return { stale: true, target: target };
13249
13775
  var resolvedTarget = target;
13250
13776
  var resolvedRunId = String((stopResult && stopResult.runId) || expectedRunId || '');
13251
13777
  var resolvedStatus = action === 'stale' ? 'running' : 'idle';
@@ -13327,6 +13853,7 @@ window.stopCurrentConversation = async function() {
13327
13853
  if (api.stopConversation) result = await api.stopConversation({ target: target, runId: runId, force: force });
13328
13854
  else if (api.abortConversation) result = await api.abortConversation(currentConversationTarget(conversationId));
13329
13855
  var resultAction = String(result && result.action || '');
13856
+ if (!conversationRunStillOwnsTarget(target, runId)) return true;
13330
13857
  if (resultAction === 'not_running' || resultAction === 'stale') {
13331
13858
  // Natural completion or a newer run won while the first stop IPC was in
13332
13859
  // flight. Reconcile from the target snapshot instead of leaving the
@@ -13338,7 +13865,7 @@ window.stopCurrentConversation = async function() {
13338
13865
  delete state.pendingProvisionalStops[runtimeKeyFor(target.workspaceId, target.conversationId)];
13339
13866
  }
13340
13867
  var latestAfterStop = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(target.workspaceId, target.conversationId)];
13341
- if (!force && latestAfterStop && String(latestAfterStop.runId || '') === runId && String(latestAfterStop.status || '') !== 'stopping') {
13868
+ if (latestAfterStop && String(latestAfterStop.runId || '') === runId && String(latestAfterStop.status || '') !== (force ? 'force_restarting' : 'stopping')) {
13342
13869
  // A second click or a terminal worker event won the race while the first
13343
13870
  // cooperative request was pending. Its older result must not downgrade
13344
13871
  // force_restarting/force_interrupted back to interrupted or running.
@@ -13348,8 +13875,9 @@ window.stopCurrentConversation = async function() {
13348
13875
  setConversationRuntimeState(target, force ? 'force_interrupted' : 'interrupted', runId);
13349
13876
  }
13350
13877
  } catch(e) {
13878
+ if (!conversationRunStillOwnsTarget(target, runId)) return false;
13351
13879
  var latestAfterError = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(target.workspaceId, target.conversationId)];
13352
- if (!force && latestAfterError && String(latestAfterError.runId || '') === runId && String(latestAfterError.status || '') !== 'stopping') {
13880
+ if (latestAfterError && String(latestAfterError.runId || '') === runId && String(latestAfterError.status || '') !== (force ? 'force_restarting' : 'stopping')) {
13353
13881
  return true;
13354
13882
  }
13355
13883
  setConversationRuntimeState(target, force ? 'stopping' : 'running', runId);
@@ -13366,6 +13894,7 @@ window.stopCurrentConversation = async function() {
13366
13894
  };
13367
13895
 
13368
13896
  window.submitCurrentAction = function(source) {
13897
+ if (state.pendingInputEdit) return window.sendMessage();
13369
13898
  if (currentFlowRunning() && flowTakeoverMatchesCurrent()) {
13370
13899
  if (!promptHasText()) {
13371
13900
  window.stopFlowRun();
@@ -13379,16 +13908,18 @@ window.submitCurrentAction = function(source) {
13379
13908
  // exits the pause and starts a fresh process. The backend agent:send /
13380
13909
  // flow:run handlers clear the suspension (without restoring mode).
13381
13910
  window.exitPausedFlowForNewInstruction();
13382
- if (state.mode === 'flow') return window.submitSelectedFlow();
13383
- return window.sendMessage('guide', guideText, { requestedMode: state.mode || 'build' });
13911
+ if (state.mode === 'flow') return window.submitSelectedFlow({ text: guideText, target: currentConversationTarget(), inputMode: state.inputMode });
13912
+ return window.sendMessage(state.inputMode, guideText, { requestedMode: state.mode || 'build', target: currentConversationTarget() });
13384
13913
  }
13385
13914
  if (state.inputMode === 'next') {
13386
- window.queueFlowNextInput(guideText);
13387
- return;
13915
+ return window.queueFlowNextInput(guideText);
13388
13916
  }
13389
- if (api.guideFlow) {
13390
- api.guideFlow(guideText).then(function(result) {
13917
+ if (api.sendMessage) {
13918
+ var flowGuideTarget = currentConversationTarget();
13919
+ return api.sendMessage(guideText, flowGuideTarget, { requestedMode: state.mode || 'flow', inputMode: 'guide', clientMessageId: window.crypto.randomUUID() }).then(function(result) {
13391
13920
  if (!result || result.ok === false) throw new Error((result && result.error) || 'Flow Guide was rejected.');
13921
+ applyConversationCommandSnapshot(result, flowGuideTarget);
13922
+ return result;
13392
13923
  }).catch(function(error) {
13393
13924
  showUiNotice(error && error.message ? error.message : String(error), 'error', 'flow-guide');
13394
13925
  });
@@ -13451,8 +13982,26 @@ function contextInspectorValue(value) {
13451
13982
  return Number.isFinite(number) ? Math.max(0, Math.round(number)) : 0;
13452
13983
  }
13453
13984
 
13454
- function contextInspectorCell(label, value) {
13455
- return '<div class="context-inspector-cell"><span class="context-inspector-label">' + esc(label) + '</span><span class="context-inspector-value">' + esc(value) + '</span></div>';
13985
+ function contextInspectorReportedValue(value) {
13986
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.round(value) : null;
13987
+ }
13988
+
13989
+ function contextInspectorCell(label, value, title) {
13990
+ return '<div class="context-inspector-cell"><span class="context-inspector-label">' + esc(label) + '</span><span class="context-inspector-value"' + (title ? ' title="' + esc(title) + '"' : '') + '>' + esc(value) + '</span></div>';
13991
+ }
13992
+
13993
+ function positionContextInspector() {
13994
+ if (!state.contextInspectorOpen) return;
13995
+ var panel = document.getElementById('context-inspector');
13996
+ var ring = els['context-token-ring'] || document.getElementById('context-token-ring');
13997
+ if (!panel || !ring || !ring.getBoundingClientRect) return;
13998
+ var topbar = document.getElementById('topbar');
13999
+ var visibleTop = topbar && topbar.getBoundingClientRect ? Math.max(0, topbar.getBoundingClientRect().bottom || 0) : 0;
14000
+ // Preserve the existing anchor and 10px gap while keeping the top visible.
14001
+ // Scroll this inspector's contents when the terminal/window leaves less room.
14002
+ panel.style.maxHeight = Math.max(0, Math.floor(ring.getBoundingClientRect().top - visibleTop - 20)) + 'px';
14003
+ panel.style.overflowY = 'auto';
14004
+ panel.style.overscrollBehavior = 'contain';
13456
14005
  }
13457
14006
 
13458
14007
  window.renderContextInspector = function() {
@@ -13478,8 +14027,39 @@ window.renderContextInspector = function() {
13478
14027
  var providerInputTokens = contextInspectorValue(c.providerInputTokens);
13479
14028
  var providerOutputTokens = contextInspectorValue(c.providerOutputTokens);
13480
14029
  var providerCacheReadTokens = contextInspectorValue(c.providerCacheReadTokens);
13481
- var providerCacheReadRatio = Math.max(0, Math.min(1, Number(c.providerCacheReadRatio) || 0));
13482
- var cacheHitPercent = Math.round(providerCacheReadRatio * 1000) / 10;
14030
+ var usageRequests = contextInspectorValue(c.providerUsageRequests);
14031
+ var inputReportedRequests = contextInspectorValue(c.providerUsageInputReportedRequests);
14032
+ var outputReportedRequests = contextInspectorValue(c.providerUsageOutputReportedRequests);
14033
+ var cacheReportedRequests = contextInspectorValue(c.providerUsageCacheReportedRequests);
14034
+ var hasLegacyTotals = c.providerUsageHasLegacyTotals === true;
14035
+ var inputAvailable = inputReportedRequests > 0 || hasLegacyTotals;
14036
+ var outputAvailable = outputReportedRequests > 0 || hasLegacyTotals;
14037
+ var usageComplete = usageRequests > 0 && inputReportedRequests === usageRequests && outputReportedRequests === usageRequests && !hasLegacyTotals;
14038
+ var conversationUsageValue = inputAvailable || outputAvailable
14039
+ ? (usageComplete ? '' : t('status.cacheReportedPortion') + ' ') + providerTotalTokens + ' · '
14040
+ + (inputAvailable ? providerInputTokens : t('status.usageUnavailable')) + ' in / '
14041
+ + (outputAvailable ? providerOutputTokens : t('status.usageUnavailable')) + ' out'
14042
+ : t('status.usageUnavailable');
14043
+ var providerCacheReadRatio = typeof c.providerCacheReadRatio === 'number' && Number.isFinite(c.providerCacheReadRatio) ? c.providerCacheReadRatio : null;
14044
+ var knownCacheReadRatio = typeof c.providerKnownCacheReadRatio === 'number' && Number.isFinite(c.providerKnownCacheReadRatio) ? c.providerKnownCacheReadRatio : null;
14045
+ var fullCacheCoverage = usageRequests > 0 && inputReportedRequests === usageRequests && cacheReportedRequests === usageRequests && !hasLegacyTotals;
14046
+ var cacheHitValue = fullCacheCoverage && providerCacheReadRatio !== null
14047
+ ? (Math.round(Math.max(0, Math.min(1, providerCacheReadRatio)) * 1000) / 10) + '% · ' + providerCacheReadTokens + ' cached'
14048
+ : t('status.usageUnavailable');
14049
+ if (!fullCacheCoverage && knownCacheReadRatio !== null) {
14050
+ cacheHitValue += ' · ' + t('status.cacheReportedPortion') + ' ' + (Math.round(Math.max(0, Math.min(1, knownCacheReadRatio)) * 1000) / 10) + '%';
14051
+ }
14052
+ var lastReportedInput = contextInspectorReportedValue(c.requestContext && c.requestContext.inputTokens);
14053
+ var lastRequestDetails = c.requestContext ? [c.requestContext.model, c.requestContext.at].filter(Boolean).join(' · ') : '';
14054
+ var estimateHint = t(c.contextEstimateSource === 'active_request' ? 'status.contextEstimateRequest' : 'status.contextEstimateHistory');
14055
+ if (c.contextEstimateHasImages) estimateHint += ' ' + t('status.contextEstimateImages');
14056
+ var usageCoverageHint = currentLang() === 'zh'
14057
+ ? '本次及已保存计量:' + usageRequests + ' 次请求;输入上报 ' + inputReportedRequests + ' 次,输出上报 ' + outputReportedRequests + ' 次,缓存明细上报 ' + cacheReportedRequests + ' 次。命中率按缓存读取 token / 输入 token 加权。'
14058
+ : 'Recorded requests: ' + usageRequests + '; input reported: ' + inputReportedRequests + '; output reported: ' + outputReportedRequests + '; cache details reported: ' + cacheReportedRequests + '. Hit rate is weighted cached input / input tokens.';
14059
+ if (hasLegacyTotals) usageCoverageHint += currentLang() === 'zh' ? ' 含旧累计用量,历史上报覆盖范围未知。' : ' Legacy totals are present; historical reporting coverage is unknown.';
14060
+ if (!fullCacheCoverage && knownCacheReadRatio !== null) usageCoverageHint += currentLang() === 'zh'
14061
+ ? ' 已上报部分的输入分母为 ' + contextInspectorValue(c.providerCacheEligibleInputTokens) + ' token。'
14062
+ : ' The reported portion covers ' + contextInspectorValue(c.providerCacheEligibleInputTokens) + ' input tokens.';
13483
14063
  var percent = Math.min(100, Math.round((used / max) * 100));
13484
14064
  var fillColor = c.warning === 'over_limit' ? 'var(--nm-state-danger)' : (c.warning === 'near_limit' ? 'var(--nm-state-warning)' : 'var(--accent)');
13485
14065
  var compression = state.contextCompression || null;
@@ -13492,25 +14072,36 @@ window.renderContextInspector = function() {
13492
14072
  '<div class="context-inspector-meter"><div class="context-inspector-meter-fill" style="width:' + percent + '%;background:' + fillColor + '"></div></div>' +
13493
14073
  '<div class="context-inspector-grid">' +
13494
14074
  contextInspectorCell(t('status.contextTokens'), used + ' / ' + max) +
13495
- contextInspectorCell(t('status.conversationTokens'), providerTotalTokens + ' · ' + providerInputTokens + ' in / ' + providerOutputTokens + ' out') +
14075
+ contextInspectorCell(t('status.conversationTokens'), conversationUsageValue) +
13496
14076
  contextInspectorCell(t('status.activeBuild'), active + ' / ' + activeTrigger) +
13497
- contextInspectorCell(t('status.cacheHitRate'), cacheHitPercent + '% · ' + providerCacheReadTokens + ' cached') +
14077
+ contextInspectorCell(t('status.cacheHitRate'), cacheHitValue) +
13498
14078
  contextInspectorCell(t('status.longHistory'), history + ' / ' + historyTrigger) +
13499
14079
  contextInspectorCell(t('status.retention'), activeRetention + ' + ' + historyRetention) +
13500
14080
  contextInspectorCell(t('status.hotCache'), contextInspectorValue(c.cacheEntries) + ' entries') +
13501
14081
  contextInspectorCell(t('status.coldArchive'), contextInspectorValue(c.archiveEntries) + ' entries') +
14082
+ contextInspectorCell(t('status.requestOverhead'), c.contextEstimateSource === 'active_request' ? contextInspectorValue(c.systemPromptTokens) + ' / ' + contextInspectorValue(c.toolSchemaTokens) : t('status.requestOverheadExcluded')) +
14083
+ contextInspectorCell(t('status.lastReportedInput'), lastReportedInput === null ? t('status.usageUnavailable') : String(lastReportedInput), lastRequestDetails) +
13502
14084
  '</div>' +
14085
+ '<div class="context-inspector-meta">' + esc(estimateHint) + '</div>' +
14086
+ '<div class="context-inspector-meta">' + esc(usageCoverageHint) + '</div>' +
13503
14087
  '<div class="context-inspector-section"><div class="context-inspector-section-title">' + esc(t('status.lastCompression')) + '</div><div class="context-inspector-meta">' + esc(last) + '</div>' +
13504
14088
  (compression && compression.model ? '<div class="context-inspector-meta">' + esc(compression.model) + '</div>' : '') + '</div>' +
13505
14089
  '<div class="context-inspector-actions"><button type="button" class="sec-btn primary" onclick="window.compressContextNow()"' + (busy || running ? ' disabled' : '') + '>' + esc(busy ? t('status.compressing') : t('status.compressNow')) + '</button></div>';
14090
+ positionContextInspector();
13506
14091
  };
13507
14092
 
13508
14093
  window.closeContextInspector = function() {
13509
- state.contextInspectorOpen = false;
13510
- window.renderContextInspector();
14094
+ if (!state.contextInspectorOpen) return;
14095
+ var panel = document.getElementById('context-inspector');
14096
+ var ring = document.getElementById('context-token-ring');
14097
+ window.animateLiquidPopupExit(panel, ring, function() {
14098
+ state.contextInspectorOpen = false;
14099
+ window.renderContextInspector();
14100
+ });
13511
14101
  };
13512
14102
 
13513
14103
  (function() {
14104
+ window.addEventListener('resize', positionContextInspector);
13514
14105
  document.addEventListener('click', function(event) {
13515
14106
  if (!state.contextInspectorOpen) return;
13516
14107
  var ring = document.getElementById('context-token-ring');
@@ -13525,6 +14116,7 @@ window.toggleContextInspector = function() {
13525
14116
  state.contextInspectorOpen = !state.contextInspectorOpen;
13526
14117
  window.hideContextWindowTooltip();
13527
14118
  window.renderContextInspector();
14119
+ if (state.contextInspectorOpen) scheduleActiveContextWindowRefresh(currentConversationTarget());
13528
14120
  };
13529
14121
 
13530
14122
  window.compressContextNow = function() {
@@ -13539,15 +14131,22 @@ window.compressContextNow = function() {
13539
14131
  }
13540
14132
  state.contextMutationPending = true;
13541
14133
  window.renderContextInspector();
13542
- Promise.resolve(api.compressContext({ target: currentConversationTarget(), force: true })).then(function(result) {
14134
+ var compressionTarget = currentConversationTarget();
14135
+ var compressionContextRequest = captureContextWindowSnapshotRequest(compressionTarget);
14136
+ return Promise.resolve(api.compressContext({ target: compressionTarget, force: true })).then(function(result) {
13543
14137
  if (!result || result.ok === false) throw new Error((result && result.error) || t('status.compressionFailed'));
13544
- if (result.contextWindow) state.contextWindow = result.contextWindow;
13545
- if (result.contextCompression !== undefined) state.contextCompression = result.contextCompression;
13546
- window.renderContextWindow();
14138
+ if (!applyContextWindowSnapshot(result, compressionContextRequest)) {
14139
+ // A concurrent read may have sampled the budget before compression
14140
+ // completed. Query again after the mutation rather than publishing an
14141
+ // old mutation response over newer usage/model state.
14142
+ if (isActiveConversationTarget(compressionTarget)) scheduleActiveContextWindowRefresh(compressionTarget);
14143
+ return result;
14144
+ }
13547
14145
  window.renderRightStatusPanel();
13548
14146
  showUiNotice(t('status.compressedNow'), 'success', 'context-compress-success');
13549
14147
  return result;
13550
14148
  }).catch(function(error) {
14149
+ if (!isCurrentContextWindowSnapshotRequest(compressionContextRequest)) return;
13551
14150
  showUiNotice(error && error.message ? error.message : t('status.compressionFailed'), 'error', 'context-compress-error');
13552
14151
  }).finally(function() {
13553
14152
  state.contextMutationPending = false;
@@ -13583,11 +14182,22 @@ window.closeModelSelectMenu = function(options) {
13583
14182
  var shell = document.getElementById('model-select-shell');
13584
14183
  var button = document.getElementById('model-select-button');
13585
14184
  var menu = document.getElementById('model-select-menu');
13586
- if (shell) shell.classList.remove('open');
13587
- if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
13588
- if (button) button.setAttribute('aria-expanded', 'false');
13589
- window.setLiquidSidebarGestureLock('popup:model-select', false);
13590
- if (options && options.focus && button) button.focus();
14185
+ var wasOpen = shell && shell.classList.contains('open') ||
14186
+ !!(menu && typeof menu.matches === 'function' && menu.matches(':popover-open'));
14187
+ if (!wasOpen) {
14188
+ if (shell) shell.classList.remove('open');
14189
+ if (button) button.setAttribute('aria-expanded', 'false');
14190
+ window.setLiquidSidebarGestureLock('popup:model-select', false);
14191
+ if (options && options.focus && button) button.focus();
14192
+ return;
14193
+ }
14194
+ window.animateLiquidPopupExit(menu, button, function() {
14195
+ if (shell) shell.classList.remove('open');
14196
+ if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
14197
+ if (button) button.setAttribute('aria-expanded', 'false');
14198
+ window.setLiquidSidebarGestureLock('popup:model-select', false);
14199
+ if (options && options.focus && button) button.focus();
14200
+ });
13591
14201
  };
13592
14202
 
13593
14203
  window.toggleModelSelectMenu = function(forceOpen) {
@@ -13628,11 +14238,21 @@ window.selectModelFromMenu = function(value) {
13628
14238
  window.closeConversationActionMenu = function(options) {
13629
14239
  var menu = document.getElementById('conversation-action-menu');
13630
14240
  var anchor = state.conversationActionAnchor;
13631
- if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
13632
- document.querySelectorAll('.conv-more-btn[aria-expanded="true"]').forEach(function(item) { item.setAttribute('aria-expanded', 'false'); });
13633
- state.conversationActionAnchor = null;
13634
- window.setLiquidSidebarGestureLock('popup:conversation-action', false);
13635
- if (options && options.focus && anchor && anchor.focus) anchor.focus();
14241
+ var wasOpen = !!(menu && typeof menu.matches === 'function' && menu.matches(':popover-open'));
14242
+ if (!wasOpen) {
14243
+ document.querySelectorAll('.conv-more-btn[aria-expanded="true"]').forEach(function(item) { item.setAttribute('aria-expanded', 'false'); });
14244
+ state.conversationActionAnchor = null;
14245
+ window.setLiquidSidebarGestureLock('popup:conversation-action', false);
14246
+ if (options && options.focus && anchor && anchor.focus) anchor.focus();
14247
+ return;
14248
+ }
14249
+ window.animateLiquidPopupExit(menu, anchor, function() {
14250
+ if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
14251
+ document.querySelectorAll('.conv-more-btn[aria-expanded="true"]').forEach(function(item) { item.setAttribute('aria-expanded', 'false'); });
14252
+ state.conversationActionAnchor = null;
14253
+ window.setLiquidSidebarGestureLock('popup:conversation-action', false);
14254
+ if (options && options.focus && anchor && anchor.focus) anchor.focus();
14255
+ });
13636
14256
  };
13637
14257
 
13638
14258
  window.positionConversationActionMenu = function(anchor, menu, pos) {
@@ -13959,6 +14579,7 @@ function createKyantLiquidRenderer() {
13959
14579
  renderFrame: 0,
13960
14580
  frameId: '',
13961
14581
  lastGeometry: null,
14582
+ lastSurfaceState: null,
13962
14583
  pendingGeometry: null,
13963
14584
  uniformState: Object.create(null),
13964
14585
  invalid: false
@@ -14002,6 +14623,7 @@ function acquireKyantLiquidRenderer(float) {
14002
14623
  renderer.displayCanvas = displayCanvas;
14003
14624
  renderer.displayContext = displayContext;
14004
14625
  renderer.lastGeometry = null;
14626
+ renderer.lastSurfaceState = null;
14005
14627
  renderer.pendingGeometry = null;
14006
14628
  return renderer;
14007
14629
  }
@@ -14018,6 +14640,7 @@ function uploadKyantLiquidFrame(renderer, frame) {
14018
14640
  // float geometry has not changed. Otherwise the geometry cache skips the
14019
14641
  // draw and leaves the previously captured backdrop on screen.
14020
14642
  renderer.lastGeometry = null;
14643
+ renderer.lastSurfaceState = null;
14021
14644
  return true;
14022
14645
  }
14023
14646
 
@@ -14077,8 +14700,13 @@ function attachKyantLiquidRenderer(float, frame) {
14077
14700
  var pixelWidth = Math.max(1, Math.round(width*renderScale));
14078
14701
  var pixelHeight = Math.max(1, Math.round(height*renderScale));
14079
14702
  var geometryKey = [left.toFixed(2),top.toFixed(2),width.toFixed(2),height.toFixed(2),pixelWidth,pixelHeight].join(':');
14080
- if (renderer.lastGeometry === geometryKey) return;
14703
+ var glow = float._liquidGlow;
14704
+ var surfaceStateKey = glow && glow.opacity > 0
14705
+ ? [glow.x.toFixed(4),glow.y.toFixed(4),glow.opacity.toFixed(3)].join(':')
14706
+ : 'no-glow';
14707
+ if (renderer.lastGeometry === geometryKey && renderer.lastSurfaceState === surfaceStateKey) return;
14081
14708
  renderer.lastGeometry = geometryKey;
14709
+ renderer.lastSurfaceState = surfaceStateKey;
14082
14710
  if (canvas.width !== pixelWidth) canvas.width = pixelWidth;
14083
14711
  if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
14084
14712
  if (displayCanvas.width !== pixelWidth) displayCanvas.width = pixelWidth;
@@ -14108,10 +14736,48 @@ function attachKyantLiquidRenderer(float, frame) {
14108
14736
  uniformChanged('chromaticAberration',[configuredChromatic],function() { gl.uniform1f(uniforms.chromaticAberration,configuredChromatic); });
14109
14737
  gl.drawArrays(gl.TRIANGLES,0,6);
14110
14738
  displayContext.clearRect(0,0,pixelWidth,pixelHeight);
14739
+ // The WebGL shader discards pixels outside the capsule, but the 2D
14740
+ // interaction glow is painted after that pass. Clip both passes to the
14741
+ // same rounded silhouette so a radial light can never bleed into the
14742
+ // surrounding page or produce a square compositor halo.
14743
+ var clipped = typeof displayContext.save === 'function' &&
14744
+ typeof displayContext.beginPath === 'function' &&
14745
+ typeof displayContext.clip === 'function' &&
14746
+ (typeof displayContext.roundRect === 'function' || typeof displayContext.arcTo === 'function');
14747
+ if (clipped) {
14748
+ displayContext.save();
14749
+ displayContext.beginPath();
14750
+ var clipRadius = Math.min(pixelWidth, pixelHeight) * 0.5;
14751
+ if (typeof displayContext.roundRect === 'function') {
14752
+ displayContext.roundRect(0, 0, pixelWidth, pixelHeight, clipRadius);
14753
+ } else {
14754
+ displayContext.moveTo(clipRadius, 0);
14755
+ displayContext.arcTo(pixelWidth, 0, pixelWidth, pixelHeight, clipRadius);
14756
+ displayContext.arcTo(pixelWidth, pixelHeight, 0, pixelHeight, clipRadius);
14757
+ displayContext.arcTo(0, pixelHeight, 0, 0, clipRadius);
14758
+ displayContext.arcTo(0, 0, pixelWidth, 0, clipRadius);
14759
+ displayContext.closePath();
14760
+ }
14761
+ displayContext.clip();
14762
+ }
14111
14763
  displayContext.drawImage(canvas,0,0);
14764
+ if (glow && glow.opacity > 0) {
14765
+ var gx = Math.max(0, Math.min(pixelWidth, glow.x * pixelWidth));
14766
+ var gy = Math.max(0, Math.min(pixelHeight, glow.y * pixelHeight));
14767
+ var radius = Math.max(18, Math.min(pixelWidth, pixelHeight) * 0.72);
14768
+ var gradient = displayContext.createRadialGradient(gx, gy, 0, gx, gy, radius);
14769
+ gradient.addColorStop(0, 'rgba(255,255,255,' + (0.08 + glow.opacity * 0.18).toFixed(3) + ')');
14770
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
14771
+ displayContext.fillStyle = gradient;
14772
+ displayContext.globalCompositeOperation = 'screen';
14773
+ displayContext.fillRect(0, 0, pixelWidth, pixelHeight);
14774
+ }
14775
+ if (clipped) displayContext.restore();
14112
14776
  float._kyantDrawReady = true;
14113
14777
  float.dataset.kyantDrawReady = 'true';
14114
- if (displayCanvas.parentNode !== float) float.appendChild(displayCanvas);
14778
+ if (displayCanvas.parentNode !== float || float.firstChild !== displayCanvas) {
14779
+ float.insertBefore(displayCanvas, float.firstChild);
14780
+ }
14115
14781
  };
14116
14782
  if (immediate) draw();
14117
14783
  else renderer.renderFrame = requestAnimationFrame(draw);
@@ -14123,6 +14789,7 @@ function attachKyantLiquidRenderer(float, frame) {
14123
14789
  if (renderer.renderFrame) cancelAnimationFrame(renderer.renderFrame);
14124
14790
  renderer.renderFrame = 0;
14125
14791
  renderer.lastGeometry = null;
14792
+ renderer.lastSurfaceState = null;
14126
14793
  renderer.pendingGeometry = null;
14127
14794
  displayCanvas.remove();
14128
14795
  renderer.displayCanvas = null;
@@ -14165,6 +14832,175 @@ function liquidInteractionEdgePx() {
14165
14832
  return liquidGlassCssNumber('--liquid-interaction-edge', 6);
14166
14833
  }
14167
14834
 
14835
+ /*
14836
+ * Popup deformation is deliberately expressed in physical CSS pixels. The
14837
+ * old implementation wrote a percentage scale directly (3.5% at full pull),
14838
+ * which made a large dialog visibly wobble much farther than a compact menu.
14839
+ * Convert a bounded edge travel back into a scale factor per axis so the
14840
+ * visual envelope stays stable regardless of the popup's dimensions. The
14841
+ * full-pull response reaches the hard 4px edge budget without exceeding it.
14842
+ */
14843
+ var LIQUID_POPUP_MAX_EDGE_JITTER_PX = 4;
14844
+ var LIQUID_HOLD_DRAG_ACTIVATION_MS = 80;
14845
+ var LIQUID_POPUP_DRAG_RESPONSE = 1;
14846
+ var LIQUID_POPUP_DRAG_DISTANCE_PX = 90;
14847
+ function clearLiquidPopupOutset(popup, immediate) {
14848
+ var feedback = popup && popup._liquidPopupFeedback;
14849
+ if (!feedback) return;
14850
+ feedback.target = 0;
14851
+ feedback.surface.style.setProperty('--liquid-popup-outset', '0px');
14852
+ feedback.outset = '0.000px';
14853
+ clearTimeout(feedback.clearTimer);
14854
+ function dispose() {
14855
+ if (popup._liquidPopupFeedback !== feedback) return;
14856
+ cancelAnimationFrame(feedback.frame);
14857
+ if (feedback.layer) feedback.layer.remove();
14858
+ popup.removeAttribute('data-liquid-popup-feedback');
14859
+ popup.style.removeProperty('--liquid-popup-outset');
14860
+ ['left', 'right', 'top', 'bottom'].forEach(function(side) {
14861
+ popup.style.removeProperty('--liquid-popup-pull-' + side);
14862
+ });
14863
+ if (immediate && !feedback.layer && popup.getAnimations) {
14864
+ popup.getAnimations().forEach(function(animation) {
14865
+ if (animation.transitionProperty === '--liquid-popup-outset') animation.cancel();
14866
+ });
14867
+ }
14868
+ popup._liquidPopupFeedback = null;
14869
+ }
14870
+ if (immediate) dispose();
14871
+ else feedback.clearTimer = setTimeout(dispose, 150);
14872
+ }
14873
+
14874
+ function setLiquidPopupOutset(popup, amount, dx, dy) {
14875
+ amount = Math.max(0, Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, amount));
14876
+ var distance = Math.hypot(dx || 0, dy || 0);
14877
+ if (!popup || !amount || !distance) { clearLiquidPopupOutset(popup, false); return; }
14878
+ var feedback = popup._liquidPopupFeedback;
14879
+ if (!feedback) {
14880
+ var style = getComputedStyle(popup);
14881
+ if (!popup.isConnected || style.display === 'none' || style.visibility === 'hidden') return;
14882
+ var nativePopover = popup.matches(':popover-open');
14883
+ var layer = null;
14884
+ if (!nativePopover) {
14885
+ layer = document.createElement('div');
14886
+ layer.className = 'liquid-popup-feedback-surface';
14887
+ layer.setAttribute('aria-hidden', 'true');
14888
+ layer.style.zIndex = style.zIndex === 'auto' ? '0' : style.zIndex;
14889
+ popup.parentNode.insertBefore(layer, popup);
14890
+ }
14891
+ var surface = layer || popup;
14892
+ surface.style.setProperty('--liquid-popup-outset', '0px');
14893
+ surface.style.setProperty('--liquid-feedback-color', style.getPropertyValue('--liquid-popup-surface'));
14894
+ surface.style.setProperty('--liquid-feedback-filter', style.backdropFilter);
14895
+ surface.style.setProperty('--liquid-feedback-rim', style.getPropertyValue('--liquid-popup-rim'));
14896
+ surface.style.setProperty('--liquid-feedback-inner-rim', style.getPropertyValue('--liquid-popup-inner-rim'));
14897
+ surface.style.setProperty('--liquid-feedback-radius', style.borderTopLeftRadius);
14898
+ feedback = { surface: surface, layer: layer, nativePopover: nativePopover, target: 0, frame: 0, clearTimer: 0, geometry: '', outset: '' };
14899
+ popup._liquidPopupFeedback = feedback;
14900
+ function frame() {
14901
+ if (popup._liquidPopupFeedback !== feedback) return;
14902
+ var currentStyle = getComputedStyle(popup);
14903
+ var rect = popup.getBoundingClientRect();
14904
+ if (!popup.isConnected || currentStyle.display === 'none' || currentStyle.visibility === 'hidden' ||
14905
+ popup.classList.contains('liquid-popup-exit') || !rect.width || !rect.height ||
14906
+ (feedback.nativePopover && !popup.matches(':popover-open'))) {
14907
+ clearLiquidPopupOutset(popup, true);
14908
+ return;
14909
+ }
14910
+ var left = rect.left, top = rect.top, width = rect.width, height = rect.height, pixelScale = 1;
14911
+ if (layer && layer.offsetParent) {
14912
+ var parent = layer.offsetParent;
14913
+ var parentRect = parent.getBoundingClientRect();
14914
+ var scaleX = parent.offsetWidth ? parentRect.width / parent.offsetWidth : 1;
14915
+ var scaleY = parent.offsetHeight ? parentRect.height / parent.offsetHeight : 1;
14916
+ scaleX = scaleX || 1; scaleY = scaleY || 1;
14917
+ left = (left - parentRect.left) / scaleX + parent.scrollLeft - parent.clientLeft;
14918
+ top = (top - parentRect.top) / scaleY + parent.scrollTop - parent.clientTop;
14919
+ width /= scaleX; height /= scaleY;
14920
+ pixelScale = Math.max(scaleX, scaleY);
14921
+ }
14922
+ var geometry = [left, top, width, height, currentStyle.opacity].join(',');
14923
+ if (feedback.geometry !== geometry) {
14924
+ feedback.geometry = geometry;
14925
+ surface.style.setProperty('--liquid-feedback-left', left + 'px');
14926
+ surface.style.setProperty('--liquid-feedback-top', top + 'px');
14927
+ surface.style.setProperty('--liquid-feedback-width', width + 'px');
14928
+ surface.style.setProperty('--liquid-feedback-height', height + 'px');
14929
+ if (layer) layer.style.opacity = currentStyle.opacity;
14930
+ }
14931
+ var outset = (feedback.target / pixelScale).toFixed(3) + 'px';
14932
+ if (feedback.outset !== outset) {
14933
+ feedback.outset = outset;
14934
+ surface.style.setProperty('--liquid-popup-outset', outset);
14935
+ }
14936
+ feedback.frame = requestAnimationFrame(frame);
14937
+ }
14938
+ /* Resolve the zero-sized feedback boundary before its first transition. */
14939
+ frame();
14940
+ if (popup._liquidPopupFeedback !== feedback) return;
14941
+ popup.setAttribute('data-liquid-popup-feedback', 'outward');
14942
+ getComputedStyle(surface).getPropertyValue('--liquid-popup-outset');
14943
+ }
14944
+ clearTimeout(feedback.clearTimer);
14945
+ /* Keep the response magnitude's existing transition. Direction changes are
14946
+ immediate, so reversing a pull never stretches the old opposite side. */
14947
+ var direction = [(dx || 0) / distance, (dy || 0) / distance];
14948
+ var sides = { left: Math.max(0, -direction[0]), right: Math.max(0, direction[0]), top: Math.max(0, -direction[1]), bottom: Math.max(0, direction[1]) };
14949
+ Object.keys(sides).forEach(function(side) {
14950
+ feedback.surface.style.setProperty('--liquid-popup-pull-' + side, sides[side].toFixed(6));
14951
+ });
14952
+ feedback.target = amount;
14953
+ }
14954
+
14955
+ function setLiquidPopupDeformation(popup, dx, dy, clear) {
14956
+ if (!popup) return;
14957
+ if (clear) {
14958
+ clearLiquidPopupOutset(popup, false);
14959
+ popup.style.setProperty('--liquid-popup-stretch', '0');
14960
+ popup.style.setProperty('--liquid-popup-squash', '0');
14961
+ popup.style.setProperty('scale', '1 1');
14962
+ popup.style.setProperty('--liquid-popup-edge-x', '0px');
14963
+ popup.style.setProperty('--liquid-popup-edge-y', '0px');
14964
+ return;
14965
+ }
14966
+ var distance = Math.hypot(dx || 0, dy || 0);
14967
+ if (!distance) return;
14968
+ var amount = Math.min(1, distance / LIQUID_POPUP_DRAG_DISTANCE_PX) * LIQUID_POPUP_DRAG_RESPONSE;
14969
+ var horizontal = Math.abs(dx || 0) >= Math.abs(dy || 0);
14970
+ var axisSize = Math.max(1, horizontal ? (popup.clientWidth || 0) : (popup.clientHeight || 0));
14971
+ var crossSize = Math.max(1, horizontal ? (popup.clientHeight || 0) : (popup.clientWidth || 0));
14972
+ /* Outward held feedback grows the optical surface, not an inward clip. */
14973
+ var edgeJitter = Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, amount * LIQUID_POPUP_MAX_EDGE_JITTER_PX);
14974
+ var crossJitter = edgeJitter * 0.24;
14975
+ var stretch = (edgeJitter * 2) / axisSize;
14976
+ var squash = -((crossJitter * 2) / crossSize);
14977
+ var stretchX = horizontal ? stretch : squash;
14978
+ var squashY = horizontal ? squash : stretch;
14979
+ popup.style.setProperty('--liquid-popup-stretch', stretchX.toFixed(6));
14980
+ popup.style.setProperty('--liquid-popup-squash', squashY.toFixed(6));
14981
+ popup.style.setProperty('scale', '1');
14982
+ popup.style.setProperty('--liquid-popup-edge-x', '0px');
14983
+ popup.style.setProperty('--liquid-popup-edge-y', '0px');
14984
+ setLiquidPopupOutset(popup, edgeJitter, dx, dy);
14985
+ }
14986
+
14987
+ function setLiquidPopupPressDeformation(popup) {
14988
+ if (!popup) return;
14989
+ clearLiquidPopupOutset(popup, true);
14990
+ var width = Math.max(1, popup.clientWidth || 1);
14991
+ var height = Math.max(1, popup.clientHeight || 1);
14992
+ /* Keep click feedback fixed-size as well; it is intentionally smaller than
14993
+ the drag budget so a tap never produces a large-dialog jump. */
14994
+ var edgeJitter = Math.min(4, LIQUID_POPUP_MAX_EDGE_JITTER_PX * 0.4);
14995
+ var stretch = (edgeJitter * 2) / width;
14996
+ var squash = -((edgeJitter * 0.24 * 2) / height);
14997
+ popup.style.setProperty('--liquid-popup-stretch', stretch.toFixed(6));
14998
+ popup.style.setProperty('--liquid-popup-squash', squash.toFixed(6));
14999
+ popup.style.setProperty('scale', '1');
15000
+ popup.style.setProperty('--liquid-popup-edge-x', (edgeJitter * 0.24).toFixed(3) + 'px');
15001
+ popup.style.setProperty('--liquid-popup-edge-y', edgeJitter.toFixed(3) + 'px');
15002
+ }
15003
+
14168
15004
  function startLiquidMotionTracking(float) {
14169
15005
  if (!float || float._liquidMotionFrame) return;
14170
15006
  var lastTime = 0;
@@ -14199,9 +15035,23 @@ function startLiquidMotionTracking(float) {
14199
15035
  var speed = reducedMotion ? 0 : Math.hypot(velocityX, velocityY);
14200
15036
  var amount = Math.max(0, Math.min(1, (speed - 8) / 700));
14201
15037
  var angle = speed > 1 ? Math.atan2(velocityY, velocityX) : 0;
15038
+ // Percentage-only velocity stretch exceeds 4px on wide capsules. Bound
15039
+ // the combined motion + boundary scales by the untransformed half-diagonal,
15040
+ // so the displacement of every corner fits the remaining pixel budget.
15041
+ var halfDiagonal = Math.max(1, Math.hypot(float.offsetWidth, float.offsetHeight) * .5);
15042
+ var boundaryX = parseFloat(float.style.getPropertyValue('--liquid-boundary-x')) || 0;
15043
+ var boundaryY = parseFloat(float.style.getPropertyValue('--liquid-boundary-y')) || 0;
15044
+ var boundaryPull = parseFloat(float.style.getPropertyValue('--liquid-boundary-pull')) || 0;
15045
+ var available = Math.max(0, LIQUID_POPUP_MAX_EDGE_JITTER_PX - Math.hypot(boundaryX, boundaryY));
15046
+ var motionStretch = amount * .075;
15047
+ var boundaryStretch = boundaryPull * .06;
15048
+ var requestedStretch = motionStretch + boundaryStretch + motionStretch * boundaryStretch;
15049
+ var budgetRatio = requestedStretch > 0 ? Math.min(1, available / halfDiagonal / requestedStretch) : 1;
14202
15050
  float.style.setProperty('--liquid-motion-angle', angle + 'rad');
14203
- float.style.setProperty('--liquid-motion-stretch', String(1 + amount * .075));
14204
- float.style.setProperty('--liquid-motion-squash', String(1 - amount * .035));
15051
+ float.style.setProperty('--liquid-motion-stretch', String(1 + motionStretch * budgetRatio));
15052
+ float.style.setProperty('--liquid-motion-squash', String(1 - amount * .035 * budgetRatio));
15053
+ float.style.setProperty('--liquid-boundary-scale-x', String(1 + boundaryStretch * budgetRatio));
15054
+ float.style.setProperty('--liquid-boundary-scale-y', String(1 - boundaryPull * .025 * budgetRatio));
14205
15055
  if (window.__liquidPerfCounts) float.dataset.liquidMotionAmount = amount.toFixed(4);
14206
15056
  if (moved && float._renderKyantGlass) float._renderKyantGlass({ left:left, top:top, width:width, height:height }, true);
14207
15057
  float._liquidMotionFrame = requestAnimationFrame(frame);
@@ -14218,6 +15068,8 @@ function stopLiquidMotionTracking(float) {
14218
15068
  float.style.removeProperty('--liquid-motion-angle');
14219
15069
  float.style.removeProperty('--liquid-motion-stretch');
14220
15070
  float.style.removeProperty('--liquid-motion-squash');
15071
+ float.style.removeProperty('--liquid-boundary-scale-x');
15072
+ float.style.removeProperty('--liquid-boundary-scale-y');
14221
15073
  }
14222
15074
 
14223
15075
  function waitForLiquidSelectionArrival(float, onArrive) {
@@ -14277,6 +15129,11 @@ function beginLiquidSelectionLift(float) {
14277
15129
 
14278
15130
  function wireLiquidMenuInteractions(menu) {
14279
15131
  if (!menu || menu.dataset.liquidInteractions === 'true') return;
15132
+ if (menu.dataset.liquidDirectOptions === 'true') return;
15133
+ menu.dataset.liquidDirectOptions = 'true';
15134
+ // Popup options now interact as their own color block. No top-layer glass
15135
+ // float is created; the shell itself owns the liquid refraction and light.
15136
+ return;
14280
15137
  menu.dataset.liquidInteractions = 'true';
14281
15138
  var pointer = null;
14282
15139
  var source = null;
@@ -14435,41 +15292,657 @@ function wireLiquidMenuInteractions(menu) {
14435
15292
  }, true);
14436
15293
  }
14437
15294
 
14438
- // The same single floating capsule is used by the left workspace rail and
14439
- // right sidebar tabs. Delegation keeps it valid while either rail rerenders.
14440
- function wireLiquidRailInteractions(container, selector) {
14441
- if (!container || container.dataset.liquidRailInteractions === 'true') return;
14442
- container.dataset.liquidRailInteractions = 'true';
15295
+ function wireDirectLiquidMenuInteractions(menu) {
15296
+ if (!menu || menu.dataset.liquidDirectOptions === 'true') return;
15297
+ menu.dataset.liquidDirectOptions = 'true';
14443
15298
  var pointer = null;
14444
15299
  var source = null;
14445
15300
  var target = null;
14446
- var float = null;
14447
- var generation = 0;
14448
- var pressX = 0;
14449
- var pressY = 0;
14450
- var gestureMoved = false;
14451
- function itemAt(event) {
15301
+ var holdTimer = 0;
15302
+ var dragging = false;
15303
+ var moved = false;
15304
+ var startX = 0;
15305
+ var startY = 0;
15306
+ function optionAt(event) {
14452
15307
  var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
14453
- var node = hit && hit.closest ? hit.closest(selector) : null;
14454
- return node && container.contains(node) && !node.disabled ? node : null;
15308
+ var node = hit && hit.closest ? hit.closest('button.model-select-menu-option, button.newmark-select-option') : null;
15309
+ return node && menu.contains(node) && !node.disabled ? node : null;
14455
15310
  }
14456
- function position(item) {
14457
- if (!item || !float) return;
14458
- float.style.setProperty('--liquid-boundary-x', '0px');
14459
- float.style.setProperty('--liquid-boundary-y', '0px');
14460
- float.style.setProperty('--liquid-boundary-pull', '0');
14461
- if (float._liquidCovered && float._liquidCovered !== item) {
14462
- float._liquidCovered.classList.remove('liquid-selection-covered');
14463
- }
14464
- if (item !== source) {
14465
- item.classList.add('liquid-selection-covered');
14466
- float._liquidCovered = item;
15311
+ function applyPress(option, event) {
15312
+ if (!option) return;
15313
+ var rect = option.getBoundingClientRect();
15314
+ option.style.setProperty('--liquid-option-origin-x', ((event.clientX - rect.left) / Math.max(1, rect.width) * 100) + '%');
15315
+ option.style.setProperty('--liquid-option-origin-y', ((event.clientY - rect.top) / Math.max(1, rect.height) * 100) + '%');
15316
+ option.style.setProperty('--liquid-option-press', '1');
15317
+ option.style.setProperty('--liquid-option-x', '0px');
15318
+ option.style.setProperty('--liquid-option-y', '0px');
15319
+ }
15320
+ function clearOption(option) {
15321
+ if (!option) return;
15322
+ option.style.removeProperty('--liquid-option-press');
15323
+ option.style.removeProperty('--liquid-option-x');
15324
+ option.style.removeProperty('--liquid-option-y');
15325
+ option.classList.remove('liquid-option-dragging');
15326
+ }
15327
+ function nearestOption(event) {
15328
+ var options = Array.from(menu.querySelectorAll('button.model-select-menu-option, button.newmark-select-option'))
15329
+ .filter(function(option) { return !option.disabled && option.getClientRects().length; });
15330
+ if (!options.length) return source;
15331
+ var nearest = options[0];
15332
+ var nearestDistance = Infinity;
15333
+ options.forEach(function(option) {
15334
+ var rect = option.getBoundingClientRect();
15335
+ var distance = Math.hypot(event.clientX - (rect.left + rect.width / 2), event.clientY - (rect.top + rect.height / 2));
15336
+ if (distance < nearestDistance) { nearest = option; nearestDistance = distance; }
15337
+ });
15338
+ return nearest;
15339
+ }
15340
+ function onPointerMove(event) {
15341
+ if (pointer === null || event.pointerId !== pointer) return;
15342
+ event.preventDefault();
15343
+ var dx = event.clientX - startX;
15344
+ var dy = event.clientY - startY;
15345
+ var distance = Math.hypot(dx, dy);
15346
+ if (!dragging) {
15347
+ if (distance < 4) return;
15348
+ moved = true;
15349
+ var resisted = Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, Math.sqrt(distance) * 1.25);
15350
+ var ratio = resisted / Math.max(1, distance);
15351
+ source.style.setProperty('--liquid-option-x', (dx * ratio) + 'px');
15352
+ source.style.setProperty('--liquid-option-y', (dy * ratio) + 'px');
15353
+ source.style.setProperty('--liquid-option-press', '0.2');
15354
+ return;
14467
15355
  }
14468
- if (float.classList.contains('liquid-selection-viewport')) {
14469
- var rect = item.getBoundingClientRect();
14470
- var edge = liquidInteractionEdgePx();
14471
- keepLiquidFloatExpansionInsideViewport(float, rect);
14472
- float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
15356
+ target = nearestOption(event);
15357
+ source.style.setProperty('--liquid-option-x', dx + 'px');
15358
+ source.style.setProperty('--liquid-option-y', dy + 'px');
15359
+ }
15360
+ function onPointerUp(event) {
15361
+ if (pointer === null || event.pointerId !== pointer) return;
15362
+ clearTimeout(holdTimer);
15363
+ holdTimer = 0;
15364
+ window.removeEventListener('pointermove', onPointerMove, true);
15365
+ window.removeEventListener('pointerup', onPointerUp, true);
15366
+ window.removeEventListener('pointercancel', onPointerCancel, true);
15367
+ try { menu.releasePointerCapture(pointer); } catch (_) {}
15368
+ var commit = dragging ? (target || source) : source;
15369
+ clearOption(source);
15370
+ pointer = null;
15371
+ source = null;
15372
+ target = null;
15373
+ dragging = false;
15374
+ moved = false;
15375
+ if (commit && commit.isConnected) commit.click();
15376
+ }
15377
+ function onPointerCancel(event) {
15378
+ if (pointer === null || event.pointerId !== pointer) return;
15379
+ clearTimeout(holdTimer);
15380
+ holdTimer = 0;
15381
+ window.removeEventListener('pointermove', onPointerMove, true);
15382
+ window.removeEventListener('pointerup', onPointerUp, true);
15383
+ window.removeEventListener('pointercancel', onPointerCancel, true);
15384
+ try { menu.releasePointerCapture(pointer); } catch (_) {}
15385
+ clearOption(source);
15386
+ pointer = null;
15387
+ source = null;
15388
+ target = null;
15389
+ dragging = false;
15390
+ moved = false;
15391
+ }
15392
+ menu.addEventListener('pointerdown', function(event) {
15393
+ var pressed = optionAt(event);
15394
+ if (!pressed || event.button !== 0) return;
15395
+ event.preventDefault();
15396
+ // The block's source is always the current selected row; the row pressed
15397
+ // by the user is the destination for the pickup flight.
15398
+ source = menu.querySelector('button.model-select-menu-option.selected, button.newmark-select-option.selected') || pressed;
15399
+ target = pressed;
15400
+ pointer = event.pointerId;
15401
+ startX = event.clientX;
15402
+ startY = event.clientY;
15403
+ moved = false;
15404
+ dragging = false;
15405
+ applyPress(source, event);
15406
+ menu.setPointerCapture(pointer);
15407
+ window.addEventListener('pointermove', onPointerMove, true);
15408
+ window.addEventListener('pointerup', onPointerUp, true);
15409
+ window.addEventListener('pointercancel', onPointerCancel, true);
15410
+ holdTimer = setTimeout(function() {
15411
+ holdTimer = 0;
15412
+ if (source && !moved && !dragging) {
15413
+ dragging = true;
15414
+ source.classList.add('liquid-option-dragging');
15415
+ source.style.setProperty('--liquid-option-press', '0.45');
15416
+ }
15417
+ }, LIQUID_HOLD_DRAG_ACTIVATION_MS);
15418
+ }, true);
15419
+ }
15420
+
15421
+ function wireDirectLiquidMenuInteractionsV2(menu) {
15422
+ if (!menu) return;
15423
+ var COLOR_BLOCK_FLIGHT_MS = 240;
15424
+ function ensureBlock() {
15425
+ var block = menu._liquidColorBlock;
15426
+ if (block && block.isConnected && block.parentNode === menu) {
15427
+ // During a source→target flight the selected state is intentionally
15428
+ // still the old option. Re-anchoring here would snap the block back to
15429
+ // that source while a select rerender is being processed.
15430
+ if (!menu.dataset.liquidPendingCommit) positionSelectedBlock();
15431
+ return;
15432
+ }
15433
+ // `innerHTML` replacement detaches children, including the color block.
15434
+ // Reuse the expando-backed node when available instead of creating a new
15435
+ // block (which also loses its in-flight geometry and transition state).
15436
+ if (!block) {
15437
+ block = document.createElement('div');
15438
+ block.className = 'liquid-menu-color-block';
15439
+ }
15440
+ menu.appendChild(block);
15441
+ menu._liquidColorBlock = block;
15442
+ if (!menu.dataset.liquidPendingCommit) positionSelectedBlock();
15443
+ }
15444
+ function positionBlock(option) {
15445
+ var block = menu._liquidColorBlock;
15446
+ if (!block || !option || !option.getClientRects().length) return;
15447
+ block.style.left = option.offsetLeft + 'px';
15448
+ block.style.top = option.offsetTop + 'px';
15449
+ block.style.width = option.offsetWidth + 'px';
15450
+ block.style.height = option.offsetHeight + 'px';
15451
+ block.style.opacity = '1';
15452
+ }
15453
+ function setBlockLight(event) {
15454
+ var block = menu._liquidColorBlock;
15455
+ if (!block || !event) return;
15456
+ // PC mouse interaction deliberately has no moving light source. Keep this
15457
+ // hook available for non-mouse pointers so the shared component remains
15458
+ // touch-capable without changing any glass geometry or material.
15459
+ block.style.setProperty('--liquid-block-light-opacity', event.pointerType === 'mouse' ? '0' : '1');
15460
+ if (event.pointerType === 'mouse') return;
15461
+ var rect = block.getBoundingClientRect();
15462
+ if (!rect.width || !rect.height) return;
15463
+ block.style.setProperty('--liquid-block-light-x', Math.max(0, Math.min(100, ((event.clientX - rect.left) / rect.width) * 100)) + '%');
15464
+ block.style.setProperty('--liquid-block-light-y', Math.max(0, Math.min(100, ((event.clientY - rect.top) / rect.height) * 100)) + '%');
15465
+ }
15466
+ function setBlockLifted(lifted) {
15467
+ var block = menu._liquidColorBlock;
15468
+ if (!block) return;
15469
+ block.classList.toggle('liquid-block-lifted', !!lifted);
15470
+ }
15471
+ function positionSelectedBlock() {
15472
+ positionBlock(menu.querySelector('button.model-select-menu-option.selected, button.newmark-select-option.selected'));
15473
+ }
15474
+ function optionAt(event) {
15475
+ var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
15476
+ var node = hit && hit.closest ? hit.closest('button.model-select-menu-option, button.newmark-select-option') : null;
15477
+ return node && menu.contains(node) && !node.disabled ? node : null;
15478
+ }
15479
+ function nearestOption(event) {
15480
+ var options = Array.from(menu.querySelectorAll('button.model-select-menu-option, button.newmark-select-option'))
15481
+ .filter(function(option) { return !option.disabled && option.getClientRects().length; });
15482
+ if (!options.length) return source;
15483
+ var nearest = options[0];
15484
+ var nearestDistance = Infinity;
15485
+ options.forEach(function(option) {
15486
+ var rect = option.getBoundingClientRect();
15487
+ var distance = Math.hypot(event.clientX - (rect.left + rect.width / 2), event.clientY - (rect.top + rect.height / 2));
15488
+ if (distance < nearestDistance) { nearest = option; nearestDistance = distance; }
15489
+ });
15490
+ return nearest;
15491
+ }
15492
+ function trackBlock(dx, dy) {
15493
+ var options = Array.from(menu.querySelectorAll('button.model-select-menu-option, button.newmark-select-option'))
15494
+ .filter(function(option) { return !option.disabled && option.getClientRects().length; });
15495
+ if (!options.length || !menu._liquidColorBlock) return;
15496
+ var first = options[0];
15497
+ var last = options[options.length - 1];
15498
+ var vertical = options.length < 2 || Math.abs(options[1].offsetTop - first.offsetTop) >= Math.abs(options[1].offsetLeft - first.offsetLeft);
15499
+ var minPos = vertical ? Math.min(first.offsetTop, last.offsetTop) : Math.min(first.offsetLeft, last.offsetLeft);
15500
+ var maxPos = vertical ? Math.max(first.offsetTop, last.offsetTop) : Math.max(first.offsetLeft, last.offsetLeft);
15501
+ var rawPos = vertical ? blockStartTop + dy : blockStartLeft + dx;
15502
+ var clampedPos = Math.max(minPos, Math.min(maxPos, rawPos));
15503
+ var boundaryDelta = rawPos - clampedPos;
15504
+ var block = menu._liquidColorBlock;
15505
+ block.style.transition = 'none';
15506
+ block.style.left = (vertical ? blockStartLeft : clampedPos) + 'px';
15507
+ block.style.top = (vertical ? clampedPos : blockStartTop) + 'px';
15508
+ return { vertical: vertical, boundaryDelta: boundaryDelta };
15509
+ }
15510
+ function animateBlockTo(option, done) {
15511
+ var block = menu._liquidColorBlock;
15512
+ if (!block || !option || !block.isConnected) {
15513
+ if (done) done();
15514
+ return;
15515
+ }
15516
+ var targetLeft = option.offsetLeft + 'px';
15517
+ var targetTop = option.offsetTop + 'px';
15518
+ var targetWidth = option.offsetWidth + 'px';
15519
+ var targetHeight = option.offsetHeight + 'px';
15520
+ var flightId = String(Date.now()) + ':' + Math.random();
15521
+ menu.dataset.liquidPendingCommit = flightId;
15522
+ if (menu._liquidColorFlightTimer) clearTimeout(menu._liquidColorFlightTimer);
15523
+ // Freeze the current (source or pointer-tracked) geometry, force a style
15524
+ // flush, then use two animation frames: the first commits the no-transition
15525
+ // source frame, the second starts the stylesheet transition. One frame is
15526
+ // not sufficient on Chromium when pointerup and the style write share the
15527
+ // same rendering update.
15528
+ freezeBlockGeometry(block);
15529
+ block.getBoundingClientRect();
15530
+ void block.offsetWidth;
15531
+ requestAnimationFrame(function() {
15532
+ if (menu.dataset.liquidPendingCommit !== flightId || !block.isConnected) return;
15533
+ requestAnimationFrame(function() {
15534
+ if (menu.dataset.liquidPendingCommit !== flightId || !block.isConnected) return;
15535
+ block.style.transition = '';
15536
+ block.style.left = targetLeft;
15537
+ block.style.top = targetTop;
15538
+ block.style.width = targetWidth;
15539
+ block.style.height = targetHeight;
15540
+ menu._liquidColorFlightTimer = setTimeout(function() {
15541
+ menu._liquidColorFlightTimer = 0;
15542
+ if (menu.dataset.liquidPendingCommit !== flightId) return;
15543
+ if (done) done();
15544
+ }, COLOR_BLOCK_FLIGHT_MS + 16);
15545
+ });
15546
+ });
15547
+ }
15548
+ function animateBlockToLongPressRow(option, done) {
15549
+ var block = menu._liquidColorBlock;
15550
+ if (!block || !option || !block.isConnected) {
15551
+ if (done) done();
15552
+ return;
15553
+ }
15554
+ var targetLeft = option.offsetLeft + 'px';
15555
+ var targetTop = option.offsetTop + 'px';
15556
+ var targetWidth = option.offsetWidth + 'px';
15557
+ var targetHeight = option.offsetHeight + 'px';
15558
+ var pickupId = ++pickupGeneration;
15559
+ setBlockLifted(true);
15560
+ freezeBlockGeometry(block);
15561
+ block.getBoundingClientRect();
15562
+ void block.offsetWidth;
15563
+ requestAnimationFrame(function() {
15564
+ if (pickupId !== pickupGeneration || !block.isConnected) return;
15565
+ requestAnimationFrame(function() {
15566
+ if (pickupId !== pickupGeneration || !block.isConnected) return;
15567
+ block.style.transition = '';
15568
+ block.style.left = targetLeft;
15569
+ block.style.top = targetTop;
15570
+ block.style.width = targetWidth;
15571
+ block.style.height = targetHeight;
15572
+ pickupTimer = setTimeout(function() {
15573
+ pickupTimer = 0;
15574
+ if (pickupId === pickupGeneration && done) done();
15575
+ }, COLOR_BLOCK_FLIGHT_MS + 16);
15576
+ });
15577
+ });
15578
+ }
15579
+ function freezeBlockGeometry(block) {
15580
+ var style = getComputedStyle(block);
15581
+ var left = style.left;
15582
+ var top = style.top;
15583
+ var width = style.width;
15584
+ var height = style.height;
15585
+ block.style.transition = 'none';
15586
+ block.style.left = left;
15587
+ block.style.top = top;
15588
+ block.style.width = width;
15589
+ block.style.height = height;
15590
+ }
15591
+ function cancelPickup() {
15592
+ pickupGeneration += 1;
15593
+ clearTimeout(pickupTimer);
15594
+ pickupTimer = 0;
15595
+ blockLiftAnimating = false;
15596
+ }
15597
+ function applyPress(option, event) {
15598
+ // Popup option content is inert. All visual feedback is carried by the
15599
+ // moving capsule block and the popup shell; never write transform/press
15600
+ // variables onto the text-bearing option itself.
15601
+ void option;
15602
+ void event;
15603
+ setLiquidPopupPressDeformation(menu.closest('.liquid-glass-popup'));
15604
+ }
15605
+ function clearOption(option) {
15606
+ if (!option) return;
15607
+ option.style.removeProperty('--liquid-option-press');
15608
+ option.style.removeProperty('--liquid-option-x');
15609
+ option.style.removeProperty('--liquid-option-y');
15610
+ option.classList.remove('liquid-option-dragging');
15611
+ }
15612
+ // `syncModelSelectSurface`/`syncNewmarkSelect` rebuild option markup with
15613
+ // `innerHTML`, which detaches the old block while leaving the expando
15614
+ // reference truthy. Always reconcile the carrier before the dataset guard
15615
+ // so a rerender gets a live block again (and an existing one is re-anchored
15616
+ // to the current selection).
15617
+ ensureBlock();
15618
+ if (menu.dataset.liquidDirectOptions === 'true') return;
15619
+ menu.dataset.liquidDirectOptions = 'true';
15620
+ var pointer = null;
15621
+ var source = null;
15622
+ var target = null;
15623
+ var holdTimer = 0;
15624
+ var dragging = false;
15625
+ var blockLiftAnimating = false;
15626
+ var pickupGeneration = 0;
15627
+ var pickupTimer = 0;
15628
+ var latestPointerEvent = null;
15629
+ var moved = false;
15630
+ var startX = 0;
15631
+ var startY = 0;
15632
+ var blockStartLeft = 0;
15633
+ var blockStartTop = 0;
15634
+ // Hidden menus have no option geometry. Once the popover has its final
15635
+ // visible width/position, establish the idle source without a fake flight
15636
+ // from zero (or from the previous opening). Active gestures own their frame.
15637
+ menu._liquidSyncSelectedBlock = function() {
15638
+ if (pointer !== null || menu.dataset.liquidPendingCommit) return;
15639
+ var block = menu._liquidColorBlock;
15640
+ if (!block || !menu.getClientRects().length) return;
15641
+ var transition = block.style.transition;
15642
+ block.style.transition = 'none';
15643
+ positionSelectedBlock();
15644
+ void block.offsetWidth;
15645
+ block.style.transition = transition;
15646
+ };
15647
+ function updatePopupShellDrag(dx, dy, clear) {
15648
+ var popup = menu.closest('.liquid-glass-popup');
15649
+ if (!popup) return;
15650
+ setLiquidPopupDeformation(popup, dx, dy, clear);
15651
+ }
15652
+ function onPointerMove(event) {
15653
+ if (pointer === null || event.pointerId !== pointer) return;
15654
+ event.preventDefault();
15655
+ latestPointerEvent = event;
15656
+ var dx = event.clientX - startX;
15657
+ var dy = event.clientY - startY;
15658
+ var distance = Math.hypot(dx, dy);
15659
+ if (!dragging) {
15660
+ moved = distance >= 4;
15661
+ return;
15662
+ }
15663
+ target = nearestOption(event);
15664
+ if (blockLiftAnimating) {
15665
+ setBlockLight(event);
15666
+ return;
15667
+ }
15668
+ var track = trackBlock(dx, dy);
15669
+ setBlockLight(event);
15670
+ // A normal rail drag stays rigid and follows only its track. Deform the
15671
+ // popup shell only when a held drag pushes beyond the rail endpoint.
15672
+ if (track && Math.abs(track.boundaryDelta) > 0.01) {
15673
+ updatePopupShellDrag(track.vertical ? 0 : track.boundaryDelta, track.vertical ? track.boundaryDelta : 0, false);
15674
+ } else {
15675
+ updatePopupShellDrag(0, 0, true);
15676
+ }
15677
+ }
15678
+ function finishAndCommit(commit) {
15679
+ clearTimeout(holdTimer);
15680
+ holdTimer = 0;
15681
+ // Release/cancel owns the flight state. Without clearing this flag here,
15682
+ // a pointerup during the pickup animation leaves the next interaction in
15683
+ // a permanently non-tracking state.
15684
+ cancelPickup();
15685
+ window.removeEventListener('pointermove', onPointerMove, true);
15686
+ window.removeEventListener('pointerup', onPointerUp, true);
15687
+ window.removeEventListener('pointercancel', onPointerCancel, true);
15688
+ try { menu.releasePointerCapture(pointer); } catch (_) {}
15689
+ var option = commit || source;
15690
+ var popup = menu.closest('.liquid-glass-popup');
15691
+ if (popup) {
15692
+ if (dragging) {
15693
+ setLiquidPopupDeformation(popup, 0, 0, true);
15694
+ } else {
15695
+ setLiquidPopupPressDeformation(popup);
15696
+ setTimeout(function() {
15697
+ if (popup.isConnected && pointer === null) {
15698
+ setLiquidPopupDeformation(popup, 0, 0, true);
15699
+ }
15700
+ }, 120);
15701
+ }
15702
+ }
15703
+ clearOption(source);
15704
+ pointer = null;
15705
+ source = null;
15706
+ target = null;
15707
+ dragging = false;
15708
+ moved = false;
15709
+ if (!option || !option.isConnected) return;
15710
+ // Let the color block finish its full source→target flight before the
15711
+ // change handler rerenders the menu. Otherwise the selected-state rebuild
15712
+ // snaps the block back to the old geometry halfway through the animation.
15713
+ var selectedBefore = menu.querySelector('button.model-select-menu-option.selected, button.newmark-select-option.selected');
15714
+ if (option && option.isConnected && selectedBefore && (selectedBefore !== option || menu._liquidColorBlock.classList.contains('liquid-block-lifted'))) {
15715
+ animateBlockTo(option, function() {
15716
+ if (!option.isConnected) {
15717
+ setBlockLifted(false);
15718
+ delete menu.dataset.liquidPendingCommit;
15719
+ return;
15720
+ }
15721
+ setBlockLifted(false);
15722
+ option.click();
15723
+ delete menu.dataset.liquidPendingCommit;
15724
+ });
15725
+ } else if (option && option.isConnected) {
15726
+ // Pointerup is followed by a trusted native click for the same gesture.
15727
+ // Suppress only that one follow-up click; keyboard activation remains a
15728
+ // normal trusted click because it does not set this pointer-owned flag.
15729
+ menu.dataset.liquidSuppressTrustedClick = 'true';
15730
+ setBlockLifted(false);
15731
+ option.click();
15732
+ setTimeout(function() { delete menu.dataset.liquidSuppressTrustedClick; }, 0);
15733
+ }
15734
+ }
15735
+ function onPointerUp(event) {
15736
+ if (pointer === null || event.pointerId !== pointer) return;
15737
+ finishAndCommit(dragging ? target : target || source);
15738
+ }
15739
+ function onPointerCancel(event) {
15740
+ if (pointer === null || event.pointerId !== pointer) return;
15741
+ clearTimeout(holdTimer);
15742
+ holdTimer = 0;
15743
+ window.removeEventListener('pointermove', onPointerMove, true);
15744
+ window.removeEventListener('pointerup', onPointerUp, true);
15745
+ window.removeEventListener('pointercancel', onPointerCancel, true);
15746
+ try { menu.releasePointerCapture(pointer); } catch (_) {}
15747
+ cancelPickup();
15748
+ if (menu._liquidColorBlock) freezeBlockGeometry(menu._liquidColorBlock);
15749
+ positionSelectedBlock();
15750
+ setBlockLifted(false);
15751
+ updatePopupShellDrag(0, 0, true);
15752
+ clearOption(source);
15753
+ pointer = null;
15754
+ source = null;
15755
+ target = null;
15756
+ dragging = false;
15757
+ moved = false;
15758
+ }
15759
+ menu.addEventListener('pointerdown', function(event) {
15760
+ var pressed = optionAt(event);
15761
+ if (!pressed || event.button !== 0) return;
15762
+ if (menu.dataset.liquidPendingCommit) return;
15763
+ event.preventDefault();
15764
+ // The capsule starts from the currently selected row and flies to the
15765
+ // row where the pointer was pressed; option content remains stationary.
15766
+ source = menu.querySelector('button.model-select-menu-option.selected, button.newmark-select-option.selected') || pressed;
15767
+ target = pressed;
15768
+ pointer = event.pointerId;
15769
+ startX = event.clientX;
15770
+ startY = event.clientY;
15771
+ latestPointerEvent = event;
15772
+ moved = false;
15773
+ dragging = false;
15774
+ applyPress(source, event);
15775
+ var block = menu._liquidColorBlock;
15776
+ if (block) {
15777
+ blockStartLeft = parseFloat(block.style.left || '0') || source.offsetLeft || 0;
15778
+ blockStartTop = parseFloat(block.style.top || '0') || source.offsetTop || 0;
15779
+ }
15780
+ menu.setPointerCapture(pointer);
15781
+ window.addEventListener('pointermove', onPointerMove, true);
15782
+ window.addEventListener('pointerup', onPointerUp, true);
15783
+ window.addEventListener('pointercancel', onPointerCancel, true);
15784
+ holdTimer = setTimeout(function() {
15785
+ holdTimer = 0;
15786
+ if (source && !moved && !dragging) {
15787
+ dragging = true;
15788
+ blockLiftAnimating = true;
15789
+ // The held press ends exactly when the selected block begins pickup.
15790
+ updatePopupShellDrag(0, 0, true);
15791
+ setBlockLifted(true);
15792
+ setBlockLight({ clientX: startX, clientY: startY, pointerType: event.pointerType });
15793
+ // The carrier starts at the currently selected row and flies to the
15794
+ // row where the long press began. Its drag baseline must then remain
15795
+ // at that destination, otherwise the first pointermove snaps it back
15796
+ // to the original selected row.
15797
+ var pickupTarget = target || source;
15798
+ animateBlockToLongPressRow(pickupTarget, function() {
15799
+ blockLiftAnimating = false;
15800
+ var landed = pickupTarget;
15801
+ blockStartLeft = landed ? landed.offsetLeft : blockStartLeft;
15802
+ blockStartTop = landed ? landed.offsetTop : blockStartTop;
15803
+ if (latestPointerEvent && pointer !== null) onPointerMove(latestPointerEvent);
15804
+ });
15805
+ }
15806
+ }, LIQUID_HOLD_DRAG_ACTIVATION_MS);
15807
+ }, true);
15808
+ // A physical mouse/touch activation produces a trusted native click after
15809
+ // pointerup. The pointer path above owns selection timing, so suppress that
15810
+ // native click while the color block is in flight. The delayed
15811
+ // `option.click()` is synthetic (`isTrusted === false`) and is intentionally
15812
+ // allowed through to the existing model/generic select commit handlers.
15813
+ menu.addEventListener('click', function(event) {
15814
+ var option = event.target && event.target.closest
15815
+ ? event.target.closest('button.model-select-menu-option, button.newmark-select-option')
15816
+ : null;
15817
+ if (!option || !menu.contains(option) || !event.isTrusted) return;
15818
+ if (!menu.dataset.liquidPendingCommit && !menu.dataset.liquidSuppressTrustedClick) return;
15819
+ event.preventDefault();
15820
+ event.stopImmediatePropagation();
15821
+ }, true);
15822
+ ensureBlock();
15823
+ }
15824
+
15825
+ window.animateLiquidPopupExit = function(popup, anchor, done) {
15826
+ if (!popup) { if (done) done(); return; }
15827
+ clearLiquidPopupOutset(popup, true);
15828
+ if (popup._liquidExitTimer) clearTimeout(popup._liquidExitTimer);
15829
+ var rect = popup.getBoundingClientRect();
15830
+ if (rect.width > 0 && rect.height > 0) {
15831
+ if (anchor && anchor.getBoundingClientRect) {
15832
+ var anchorRect = anchor.getBoundingClientRect();
15833
+ popup.style.setProperty(
15834
+ '--liquid-popup-origin-x',
15835
+ ((anchorRect.left + anchorRect.width / 2 - rect.left) / rect.width * 100) + '%',
15836
+ );
15837
+ popup.style.setProperty(
15838
+ '--liquid-popup-origin-y',
15839
+ ((anchorRect.top + anchorRect.height / 2 - rect.top) / rect.height * 100) + '%',
15840
+ );
15841
+ } else {
15842
+ popup.style.setProperty('--liquid-popup-origin-x', '50%');
15843
+ popup.style.setProperty('--liquid-popup-origin-y', '50%');
15844
+ }
15845
+ }
15846
+ popup.classList.add('liquid-popup-exit');
15847
+ popup._liquidExitTimer = setTimeout(function() {
15848
+ popup._liquidExitTimer = 0;
15849
+ popup.classList.remove('liquid-popup-exit');
15850
+ popup.style.removeProperty('--liquid-popup-origin-x');
15851
+ popup.style.removeProperty('--liquid-popup-origin-y');
15852
+ if (done) done();
15853
+ }, 210);
15854
+ };
15855
+
15856
+ function wireDirectPopupOptionPress() {
15857
+ if (document.documentElement.dataset.directPopupOptionPress === 'true') return;
15858
+ document.documentElement.dataset.directPopupOptionPress = 'true';
15859
+ var optionSelector = '.conv-action-menu-item, .command-option, .workspace-type-option';
15860
+ function optionAt(event) {
15861
+ var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
15862
+ return hit && hit.closest ? hit.closest(optionSelector) : null;
15863
+ }
15864
+ function applyPress(option, event) {
15865
+ var rect = option.getBoundingClientRect();
15866
+ option.style.setProperty('--liquid-option-origin-x', ((event.clientX - rect.left) / Math.max(1, rect.width) * 100) + '%');
15867
+ option.style.setProperty('--liquid-option-origin-y', ((event.clientY - rect.top) / Math.max(1, rect.height) * 100) + '%');
15868
+ option.style.setProperty('--liquid-option-press', '1');
15869
+ }
15870
+ function clearOption(option) {
15871
+ if (!option) return;
15872
+ option.style.removeProperty('--liquid-option-press');
15873
+ option.style.removeProperty('--liquid-option-x');
15874
+ option.style.removeProperty('--liquid-option-y');
15875
+ }
15876
+ document.addEventListener('pointerdown', function(event) {
15877
+ var option = optionAt(event);
15878
+ if (!option || option.disabled || event.button !== 0) return;
15879
+ var menu = option.closest('.model-select-menu, .newmark-select-menu');
15880
+ if (menu && menu.dataset.liquidDirectOptions === 'true') return;
15881
+ applyPress(option, event);
15882
+ // Content never follows the pointer inside a popup. The shell owns any
15883
+ // boundary/light response; this handler only supplies click-center scale.
15884
+ return;
15885
+ var startX = event.clientX;
15886
+ var startY = event.clientY;
15887
+ function onMove(move) {
15888
+ var dx = move.clientX - startX;
15889
+ var dy = move.clientY - startY;
15890
+ var distance = Math.hypot(dx, dy);
15891
+ if (distance < 4) return;
15892
+ move.preventDefault();
15893
+ var resisted = Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, Math.sqrt(distance) * 1.25);
15894
+ var ratio = resisted / Math.max(1, distance);
15895
+ option.style.setProperty('--liquid-option-x', (dx * ratio) + 'px');
15896
+ option.style.setProperty('--liquid-option-y', (dy * ratio) + 'px');
15897
+ option.style.setProperty('--liquid-option-press', '0.2');
15898
+ }
15899
+ function clear() {
15900
+ document.removeEventListener('pointermove', onMove, true);
15901
+ document.removeEventListener('pointerup', clear, true);
15902
+ document.removeEventListener('pointercancel', clear, true);
15903
+ clearOption(option);
15904
+ }
15905
+ document.addEventListener('pointermove', onMove, true);
15906
+ document.addEventListener('pointerup', clear, true);
15907
+ document.addEventListener('pointercancel', clear, true);
15908
+ }, true);
15909
+ }
15910
+
15911
+ // The same single floating capsule is used by the left workspace rail and
15912
+ // right sidebar tabs. Delegation keeps it valid while either rail rerenders.
15913
+ function wireLiquidRailInteractions(container, selector) {
15914
+ if (!container || container.dataset.liquidRailInteractions === 'true') return;
15915
+ container.dataset.liquidRailInteractions = 'true';
15916
+ var pointer = null;
15917
+ var source = null;
15918
+ var target = null;
15919
+ var float = null;
15920
+ var generation = 0;
15921
+ var pressX = 0;
15922
+ var pressY = 0;
15923
+ var gestureMoved = false;
15924
+ function itemAt(event) {
15925
+ var hit = event && document.elementFromPoint ? document.elementFromPoint(event.clientX, event.clientY) : event.target;
15926
+ var node = hit && hit.closest ? hit.closest(selector) : null;
15927
+ return node && container.contains(node) && !node.disabled ? node : null;
15928
+ }
15929
+ function position(item) {
15930
+ if (!item || !float) return;
15931
+ float.style.setProperty('--liquid-boundary-x', '0px');
15932
+ float.style.setProperty('--liquid-boundary-y', '0px');
15933
+ float.style.setProperty('--liquid-boundary-pull', '0');
15934
+ if (float._liquidCovered && float._liquidCovered !== item) {
15935
+ float._liquidCovered.classList.remove('liquid-selection-covered');
15936
+ }
15937
+ if (item !== source) {
15938
+ item.classList.add('liquid-selection-covered');
15939
+ float._liquidCovered = item;
15940
+ }
15941
+ if (float.classList.contains('liquid-selection-viewport')) {
15942
+ var rect = item.getBoundingClientRect();
15943
+ var edge = liquidInteractionEdgePx();
15944
+ keepLiquidFloatExpansionInsideViewport(float, rect);
15945
+ float._liquidTargetRect = { left: rect.left - edge, top: rect.top - edge, width: rect.width + edge * 2, height: rect.height + edge * 2 };
14473
15946
  float.style.left = (rect.left - edge) + 'px';
14474
15947
  float.style.top = (rect.top - edge) + 'px';
14475
15948
  float.style.width = (rect.width + edge * 2) + 'px';
@@ -14510,7 +15983,7 @@ function wireLiquidRailInteractions(container, selector) {
14510
15983
  var maxCenter = Math.max.apply(null, centers);
14511
15984
  var projectedCenter = Math.max(minCenter, Math.min(maxCenter, pointerAxis));
14512
15985
  var blockedDelta = pointerAxis < minCenter ? pointerAxis - minCenter : pointerAxis > maxCenter ? pointerAxis - maxCenter : 0;
14513
- var resisted = Math.sign(blockedDelta) * Math.sqrt(Math.abs(blockedDelta)) * 0.6;
15986
+ var resisted = Math.sign(blockedDelta) * Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, Math.sqrt(Math.abs(blockedDelta)) * 0.6);
14514
15987
  var edge = liquidInteractionEdgePx();
14515
15988
  var left = horizontal ? projectedCenter - rect.width / 2 - edge : rect.left - edge;
14516
15989
  var top = horizontal ? rect.top - edge : projectedCenter - rect.height / 2 - edge;
@@ -14697,7 +16170,7 @@ function wireLegacyLiquidButtons() {
14697
16170
  var distance = Math.hypot(dx, dy);
14698
16171
  if (distance < 2) return;
14699
16172
  move.preventDefault();
14700
- var resistedDistance = Math.min(4.5, Math.sqrt(distance) * 0.625);
16173
+ var resistedDistance = Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, Math.sqrt(distance) * 1.25);
14701
16174
  var ratio = resistedDistance / Math.max(1, distance);
14702
16175
  float.style.setProperty('--liquid-boundary-x', (dx * ratio).toFixed(2) + 'px');
14703
16176
  float.style.setProperty('--liquid-boundary-y', (dy * ratio).toFixed(2) + 'px');
@@ -14749,7 +16222,9 @@ function wireFixedPrimaryLiquidButtons() {
14749
16222
  '.msg-action-btn', '.conv-more-btn', '.provider-icon-btn',
14750
16223
  '.work-review-toggle', '.conversation-work-run-head',
14751
16224
  '.shell-block-header', '.diff-block-header', '.work-review-head',
14752
- '.message-edit-submit', '.message-edit-cancel'
16225
+ '.message-edit-submit', '.message-edit-cancel',
16226
+ '.model-select-menu-option', '.newmark-select-option',
16227
+ '.conv-action-menu-item', '.command-option', '.workspace-type-option'
14753
16228
  ].join(',');
14754
16229
  document.addEventListener('pointerdown', function(event) {
14755
16230
  var source = event.target && event.target.closest ? event.target.closest(selector) : null;
@@ -14788,7 +16263,7 @@ function wireFixedPrimaryLiquidButtons() {
14788
16263
  var distance = Math.hypot(dx, dy);
14789
16264
  if (distance < 2) return;
14790
16265
  value.preventDefault();
14791
- var resistedDistance = Math.min(4.5, Math.sqrt(distance) * .625);
16266
+ var resistedDistance = Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, Math.sqrt(distance) * 1.25);
14792
16267
  var ratio = resistedDistance / Math.max(1, distance);
14793
16268
  float.style.setProperty('--liquid-boundary-x', (dx * ratio).toFixed(2) + 'px');
14794
16269
  float.style.setProperty('--liquid-boundary-y', (dy * ratio).toFixed(2) + 'px');
@@ -14844,6 +16319,267 @@ function wireFixedPrimaryLiquidButtons() {
14844
16319
  }, true);
14845
16320
  }
14846
16321
 
16322
+ function wireLiquidPopupShellInteractions() {
16323
+ var active = null;
16324
+ var clearTimer = 0;
16325
+ function popupAt(target) {
16326
+ return target && target.closest ? target.closest('.liquid-glass-popup') : null;
16327
+ }
16328
+ function ensureOpticalCanvas(popup) {
16329
+ if (!popup) return null;
16330
+ var canvas = popup._liquidPopupOpticalCanvas;
16331
+ if (canvas && canvas.isConnected && canvas.parentNode === popup) return canvas;
16332
+ canvas = document.createElement('canvas');
16333
+ canvas.className = 'liquid-popup-optical-canvas';
16334
+ popup.insertBefore(canvas, popup.firstChild);
16335
+ popup._liquidPopupOpticalCanvas = canvas;
16336
+ popup._liquidPopupLight = { x: 0.5, y: 0.5, opacity: 0 };
16337
+ popup._liquidPopupLightPending = null;
16338
+ popup._liquidPopupLightFrame = 0;
16339
+ popup._liquidPopupDrawQueued = false;
16340
+ function resize() {
16341
+ if (!canvas.isConnected) return;
16342
+ var width = Math.max(1, popup.clientWidth);
16343
+ var height = Math.max(1, popup.clientHeight);
16344
+ var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
16345
+ canvas.width = Math.max(1, Math.round(width * dpr));
16346
+ canvas.height = Math.max(1, Math.round(height * dpr));
16347
+ canvas.style.width = width + 'px';
16348
+ canvas.style.height = height + 'px';
16349
+ queuePopupOptics(popup);
16350
+ }
16351
+ if (window.ResizeObserver) {
16352
+ popup._liquidPopupOpticalObserver = new ResizeObserver(resize);
16353
+ popup._liquidPopupOpticalObserver.observe(popup);
16354
+ }
16355
+ resize();
16356
+ return canvas;
16357
+ }
16358
+ function ensureAllPopupCanvases(root) {
16359
+ (root || document).querySelectorAll('.liquid-glass-popup').forEach(function(popup) {
16360
+ ensureOpticalCanvas(popup);
16361
+ });
16362
+ }
16363
+ function drawPopupOptics(popup) {
16364
+ var canvas = popup && popup._liquidPopupOpticalCanvas;
16365
+ if (!canvas) return;
16366
+ var context = canvas.getContext('2d');
16367
+ if (!context) return;
16368
+ var light = popup._liquidPopupLight || { x: 0.5, y: 0.5, opacity: 0 };
16369
+ var width = canvas.width;
16370
+ var height = canvas.height;
16371
+ context.clearRect(0, 0, width, height);
16372
+ if (!(light.opacity > 0)) return;
16373
+ var popupCssWidth = Math.max(1, popup.clientWidth || canvas.clientWidth || 1);
16374
+ var canvasScale = width / popupCssWidth;
16375
+ var clipped = typeof context.save === 'function' &&
16376
+ typeof context.beginPath === 'function' &&
16377
+ typeof context.clip === 'function' &&
16378
+ (typeof context.roundRect === 'function' || typeof context.arcTo === 'function');
16379
+ if (clipped) {
16380
+ context.save();
16381
+ context.beginPath();
16382
+ var popupStyle = getComputedStyle(popup);
16383
+ var popupRadiusCss = parseFloat(popupStyle.borderTopLeftRadius) || liquidGlassCssNumber('--liquid-popup-radius', 24);
16384
+ var radius = Math.min(Math.max(0, popupRadiusCss * canvasScale), Math.min(width, height) * 0.5);
16385
+ if (typeof context.roundRect === 'function') {
16386
+ context.roundRect(0, 0, width, height, radius);
16387
+ } else {
16388
+ context.moveTo(radius, 0);
16389
+ context.arcTo(width, 0, width, height, radius);
16390
+ context.arcTo(width, height, 0, height, radius);
16391
+ context.arcTo(0, height, 0, 0, radius);
16392
+ context.arcTo(0, 0, width, 0, radius);
16393
+ context.closePath();
16394
+ }
16395
+ context.clip();
16396
+ }
16397
+ // Keep the interaction spot in physical CSS pixels. The previous
16398
+ // min(width,height) multiplier made a large dialog produce a huge light
16399
+ // field while a compact menu barely showed one. The backing canvas is
16400
+ // DPR-scaled, so convert the fixed 20px CSS radius to canvas pixels.
16401
+ var configuredRadius = liquidGlassCssNumber('--liquid-popup-glow-radius', 20);
16402
+ var radius = Math.max(1, configuredRadius * canvasScale);
16403
+ var gradient = context.createRadialGradient(light.x * width, light.y * height, 0, light.x * width, light.y * height, radius);
16404
+ gradient.addColorStop(0, 'rgba(255,255,255,' + (0.08 + light.opacity * 0.16).toFixed(3) + ')');
16405
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
16406
+ context.fillStyle = gradient;
16407
+ context.fillRect(0, 0, width, height);
16408
+ if (clipped) context.restore();
16409
+ }
16410
+ function queuePopupOptics(popup) {
16411
+ if (!popup || popup._liquidPopupDrawQueued) return;
16412
+ popup._liquidPopupDrawQueued = true;
16413
+ requestAnimationFrame(function() {
16414
+ popup._liquidPopupDrawQueued = false;
16415
+ if (popup.isConnected) drawPopupOptics(popup);
16416
+ });
16417
+ }
16418
+ function updateLight(popup, event, opacity) {
16419
+ if (!popup) return;
16420
+ // PC mouse pointers do not paint an interaction bloom. Keep the popup
16421
+ // shell, refraction and deformation active; only clear the pointer light.
16422
+ if (event && event.pointerType === 'mouse') {
16423
+ popup._liquidPopupLightPending = null;
16424
+ if (popup._liquidPopupLight) popup._liquidPopupLight.opacity = 0;
16425
+ popup.style.setProperty('--liquid-popup-light-opacity', '0');
16426
+ queuePopupOptics(popup);
16427
+ return;
16428
+ }
16429
+ ensureOpticalCanvas(popup);
16430
+ var rect = popup.getBoundingClientRect();
16431
+ if (!rect.width || !rect.height) return;
16432
+ var x = Math.max(0, Math.min(100, (event.clientX - rect.left) / rect.width * 100));
16433
+ var y = Math.max(0, Math.min(100, (event.clientY - rect.top) / rect.height * 100));
16434
+ popup._liquidPopupLightPending = { x: x / 100, y: y / 100, opacity: opacity };
16435
+ if (popup._liquidPopupLightFrame) return;
16436
+ popup._liquidPopupLightFrame = requestAnimationFrame(function() {
16437
+ popup._liquidPopupLightFrame = 0;
16438
+ if (!popup.isConnected) return;
16439
+ var pending = popup._liquidPopupLightPending;
16440
+ if (!pending) return;
16441
+ popup._liquidPopupLightPending = null;
16442
+ popup.style.setProperty('--liquid-light-x', (pending.x * 100).toFixed(2) + '%');
16443
+ popup.style.setProperty('--liquid-light-y', (pending.y * 100).toFixed(2) + '%');
16444
+ popup.style.setProperty('--liquid-popup-light-opacity', String(pending.opacity));
16445
+ popup._liquidPopupLight = pending;
16446
+ queuePopupOptics(popup);
16447
+ });
16448
+ }
16449
+ function clearPopup(popup) {
16450
+ if (!popup) return;
16451
+ popup.style.setProperty('--liquid-popup-light-opacity', '0');
16452
+ setLiquidPopupDeformation(popup, 0, 0, true);
16453
+ if (popup._liquidPopupLight) popup._liquidPopupLight.opacity = 0;
16454
+ popup._liquidPopupLightPending = null;
16455
+ if (popup._liquidPopupLightFrame) {
16456
+ cancelAnimationFrame(popup._liquidPopupLightFrame);
16457
+ popup._liquidPopupLightFrame = 0;
16458
+ }
16459
+ queuePopupOptics(popup);
16460
+ }
16461
+ function scheduleClear(popup, delay) {
16462
+ if (clearTimer) clearTimeout(clearTimer);
16463
+ clearTimer = setTimeout(function() {
16464
+ clearTimer = 0;
16465
+ clearPopup(popup);
16466
+ }, delay);
16467
+ }
16468
+ document.addEventListener('pointerover', function(event) {
16469
+ var popup = popupAt(event.target);
16470
+ if (popup) updateLight(popup, event, 0.35);
16471
+ }, true);
16472
+ document.addEventListener('toggle', function(event) {
16473
+ var popup = popupAt(event.target);
16474
+ if (popup && event.newState === 'closed') clearLiquidPopupOutset(popup, true);
16475
+ if (popup && event.newState === 'open') ensureOpticalCanvas(popup);
16476
+ }, true);
16477
+ ensureAllPopupCanvases(document);
16478
+ if (window.MutationObserver && document.body) {
16479
+ var popupObserver = new MutationObserver(function(records) {
16480
+ records.forEach(function(record) {
16481
+ Array.from(record.addedNodes || []).forEach(function(node) {
16482
+ if (node.nodeType !== 1) return;
16483
+ if (node.matches && node.matches('.liquid-glass-popup')) ensureOpticalCanvas(node);
16484
+ ensureAllPopupCanvases(node);
16485
+ });
16486
+ });
16487
+ });
16488
+ popupObserver.observe(document.body, { childList: true, subtree: true });
16489
+ }
16490
+ document.addEventListener('pointermove', function(event) {
16491
+ var popup = popupAt(event.target);
16492
+ if (popup && (!active || active.popup !== popup)) updateLight(popup, event, 0.35);
16493
+ }, true);
16494
+ document.addEventListener('pointerdown', function(event) {
16495
+ var popup = popupAt(event.target);
16496
+ if (!popup) return;
16497
+ if (clearTimer) { clearTimeout(clearTimer); clearTimer = 0; }
16498
+ active = { popup: popup, x: event.clientX, y: event.clientY, pointer: event.pointerId, startedAt: performance.now(), trackOwned: popup.dataset.liquidDirectOptions === 'true' };
16499
+ updateLight(popup, event, 1);
16500
+ // A click still gives the carrier shell a short, visible deformation even
16501
+ // when the pointer does not travel far enough to enter hold-drag mode.
16502
+ if (!active.trackOwned) setLiquidPopupPressDeformation(popup);
16503
+ }, true);
16504
+ window.addEventListener('pointermove', function(event) {
16505
+ if (!active || event.pointerId !== active.pointer) return;
16506
+ if (!active.popup || !active.popup.isConnected) {
16507
+ clearPopup(active.popup);
16508
+ active = null;
16509
+ return;
16510
+ }
16511
+ var dx = event.clientX - active.x;
16512
+ var dy = event.clientY - active.y;
16513
+ // A menu owns its rail and endpoint resistance. The generic shell may
16514
+ // respond only to a held gesture without a movement track.
16515
+ if (!active.trackOwned && performance.now() - active.startedAt >= LIQUID_HOLD_DRAG_ACTIVATION_MS) {
16516
+ setLiquidPopupDeformation(active.popup, dx, dy, false);
16517
+ }
16518
+ updateLight(active.popup, event, 1);
16519
+ }, true);
16520
+ window.addEventListener('pointerup', function() {
16521
+ if (!active) return;
16522
+ scheduleClear(active.popup, 120);
16523
+ active = null;
16524
+ }, true);
16525
+ window.addEventListener('pointercancel', function() {
16526
+ if (!active) return;
16527
+ clearPopup(active.popup);
16528
+ if (clearTimer) { clearTimeout(clearTimer); clearTimer = 0; }
16529
+ active = null;
16530
+ }, true);
16531
+ function cancelPopupFeedback() {
16532
+ if (active) {
16533
+ clearLiquidPopupOutset(active.popup, true);
16534
+ clearPopup(active.popup);
16535
+ active = null;
16536
+ }
16537
+ if (clearTimer) { clearTimeout(clearTimer); clearTimer = 0; }
16538
+ }
16539
+ window.addEventListener('blur', cancelPopupFeedback, true);
16540
+ window.addEventListener('pagehide', cancelPopupFeedback, true);
16541
+ }
16542
+
16543
+ function wireLiquidFloatGlowInteractions() {
16544
+ if (document.documentElement.dataset.liquidFloatGlowWired === 'true') return;
16545
+ document.documentElement.dataset.liquidFloatGlowWired = 'true';
16546
+ function activeFloats() {
16547
+ return Array.from(document.querySelectorAll('.liquid-selection-float.visible, .liquid-selection-float.liquid-option-dragging'))
16548
+ .filter(function(float) { return float.isConnected; });
16549
+ }
16550
+ function update(float, event, opacity) {
16551
+ if (!float || !float.isConnected) return;
16552
+ if (event && event.pointerType === 'mouse') {
16553
+ if (float._liquidGlow && float._liquidGlow.opacity > 0) {
16554
+ float._liquidGlow = { x: 0.5, y: 0.5, opacity: 0 };
16555
+ if (float._renderKyantGlass) float._renderKyantGlass(null, true);
16556
+ }
16557
+ return;
16558
+ }
16559
+ var rect = float.getBoundingClientRect();
16560
+ if (!rect.width || !rect.height) return;
16561
+ float._liquidGlow = {
16562
+ x: Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)),
16563
+ y: Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height)),
16564
+ opacity: opacity,
16565
+ };
16566
+ if (float._renderKyantGlass) float._renderKyantGlass(null, true);
16567
+ }
16568
+ function clear(float) {
16569
+ if (!float) return;
16570
+ float._liquidGlow = { x: 0.5, y: 0.5, opacity: 0 };
16571
+ if (float._renderKyantGlass) float._renderKyantGlass(null, true);
16572
+ }
16573
+ document.addEventListener('pointermove', function(event) {
16574
+ activeFloats().forEach(function(float) { update(float, event, 0.5); });
16575
+ }, true);
16576
+ document.addEventListener('pointerdown', function(event) {
16577
+ activeFloats().forEach(function(float) { update(float, event, 1); });
16578
+ }, true);
16579
+ document.addEventListener('pointerup', function() { activeFloats().forEach(clear); }, true);
16580
+ document.addEventListener('pointercancel', function() { activeFloats().forEach(clear); }, true);
16581
+ }
16582
+
14847
16583
  requestAnimationFrame(function() {
14848
16584
  document.querySelectorAll('.liquid-click-source').forEach(function(source) {
14849
16585
  source.classList.remove('liquid-click-source');
@@ -14860,6 +16596,9 @@ requestAnimationFrame(function() {
14860
16596
  wireLiquidRailInteractions(document.getElementById('right-tabs'), '.tab-btn[data-tab]');
14861
16597
  wireLegacyLiquidButtons();
14862
16598
  wireFixedPrimaryLiquidButtons();
16599
+ wireLiquidPopupShellInteractions();
16600
+ wireLiquidFloatGlowInteractions();
16601
+ wireDirectPopupOptionPress();
14863
16602
  });
14864
16603
 
14865
16604
  // Checkbox switches use one pointer-captured glass thumb that follows the
@@ -15015,8 +16754,14 @@ window.syncModelSelectSurface = function() {
15015
16754
  html += '<button type="button" class="model-select-menu-option' + (child.value === sel.value ? ' selected' : '') + '" role="option" aria-selected="' + (child.value === sel.value ? 'true' : 'false') + '" data-value="' + escAttr(child.value) + '" onclick="window.selectModelFromMenu(this.getAttribute(\'data-value\'))">' + esc(child.textContent || child.value) + '</button>';
15016
16755
  }
15017
16756
  });
16757
+ // Rebuilding the option list must not destroy the carrier block. A pending
16758
+ // source→target flight intentionally keeps the block's interpolated inline
16759
+ // geometry while the selected state is updated only after the flight ends.
16760
+ var colorBlock = menu._liquidColorBlock;
16761
+ if (colorBlock && colorBlock.parentNode === menu) colorBlock.remove();
15018
16762
  menu.innerHTML = html;
15019
- wireLiquidMenuInteractions(menu);
16763
+ if (colorBlock) menu.appendChild(colorBlock);
16764
+ wireDirectLiquidMenuInteractionsV2(menu);
15020
16765
  };
15021
16766
 
15022
16767
  document.addEventListener('pointerdown', function(event) {
@@ -15027,13 +16772,23 @@ document.addEventListener('pointerdown', function(event) {
15027
16772
  window.closeNewmarkSelect = function(shell, options) {
15028
16773
  shell = shell || document.querySelector('.newmark-select-shell.open');
15029
16774
  if (!shell) return;
15030
- shell.classList.remove('open');
15031
- window.setLiquidSidebarGestureLock('popup:select:' + (shell.dataset.selectId || 'anonymous'), false);
15032
16775
  var menu = shell.querySelector('.newmark-select-menu');
15033
- if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
15034
16776
  var button = shell.querySelector('.newmark-select-button');
15035
- if (button) button.setAttribute('aria-expanded', 'false');
15036
- if (options && options.focus && button) button.focus();
16777
+ var wasOpen = !!(menu && typeof menu.matches === 'function' && menu.matches(':popover-open'));
16778
+ if (!wasOpen) {
16779
+ shell.classList.remove('open');
16780
+ window.setLiquidSidebarGestureLock('popup:select:' + (shell.dataset.selectId || 'anonymous'), false);
16781
+ if (button) button.setAttribute('aria-expanded', 'false');
16782
+ if (options && options.focus && button) button.focus();
16783
+ return;
16784
+ }
16785
+ window.animateLiquidPopupExit(menu, button, function() {
16786
+ shell.classList.remove('open');
16787
+ window.setLiquidSidebarGestureLock('popup:select:' + (shell.dataset.selectId || 'anonymous'), false);
16788
+ if (menu && typeof menu.hidePopover === 'function' && menu.matches(':popover-open')) menu.hidePopover();
16789
+ if (button) button.setAttribute('aria-expanded', 'false');
16790
+ if (options && options.focus && button) button.focus();
16791
+ });
15037
16792
  };
15038
16793
 
15039
16794
  window.positionSelectPopup = function(button, menu) {
@@ -15054,6 +16809,7 @@ window.positionSelectPopup = function(button, menu) {
15054
16809
  menu.style.left = left + 'px';
15055
16810
  menu.style.top = (openAbove ? rect.top - visibleHeight - margin : rect.bottom + margin) + 'px';
15056
16811
  menu.dataset.popupDirection = openAbove ? 'up' : 'down';
16812
+ if (menu._liquidSyncSelectedBlock) menu._liquidSyncSelectedBlock();
15057
16813
  };
15058
16814
 
15059
16815
  window.positionNewmarkSelectMenu = function(shell) {
@@ -15080,10 +16836,12 @@ window.syncNewmarkSelectWidth = function(select, shell) {
15080
16836
  if (!select || !shell || shell.dataset.autoReadableWidth !== 'true') return;
15081
16837
  var nativeWidth = Number(shell.dataset.nativeSelectWidth || 0);
15082
16838
  var width = Math.max(nativeWidth, window.selectReadableControlWidth(select));
15083
- shell.style.width = width + 'px';
15084
- shell.style.minWidth = width + 'px';
15085
- shell.style.maxWidth = 'none';
15086
- if (shell.dataset.toolbarSelect === 'true') shell.style.flex = '0 0 ' + width + 'px';
16839
+ var widthCss = width + 'px';
16840
+ if (shell.style.width !== widthCss) shell.style.width = widthCss;
16841
+ if (shell.style.minWidth !== widthCss) shell.style.minWidth = widthCss;
16842
+ if (shell.style.maxWidth !== 'none') shell.style.maxWidth = 'none';
16843
+ var flexCss = '0 0 ' + widthCss;
16844
+ if (shell.dataset.toolbarSelect === 'true' && shell.style.flex !== flexCss) shell.style.flex = flexCss;
15087
16845
  };
15088
16846
 
15089
16847
  window.syncNewmarkSelect = function(select) {
@@ -15093,9 +16851,11 @@ window.syncNewmarkSelect = function(select) {
15093
16851
  var menu = shell.querySelector('.newmark-select-menu');
15094
16852
  var button = shell.querySelector('.newmark-select-button');
15095
16853
  var selected = select.options[select.selectedIndex] || select.options[0];
15096
- shell.style.display = select.dataset.newmarkVisible === 'false' || select.style.display === 'none' ? 'none' : '';
15097
- if (label) label.textContent = selected ? selected.textContent : '';
15098
- if (button) button.disabled = !!select.disabled;
16854
+ var display = select.dataset.newmarkVisible === 'false' || select.style.display === 'none' ? 'none' : '';
16855
+ if (shell.style.display !== display) shell.style.display = display;
16856
+ var selectedText = selected ? selected.textContent : '';
16857
+ if (label && label.textContent !== selectedText) label.textContent = selectedText;
16858
+ if (button && button.disabled !== !!select.disabled) button.disabled = !!select.disabled;
15099
16859
  window.syncNewmarkSelectWidth(select, shell);
15100
16860
  if (!menu) return;
15101
16861
  var html = '';
@@ -15111,8 +16871,18 @@ window.syncNewmarkSelect = function(select) {
15111
16871
  html += '<button type="button" class="model-select-menu-option newmark-select-option' + (child.value === select.value ? ' selected' : '') + '" role="option" aria-selected="' + (child.value === select.value ? 'true' : 'false') + '" data-value="' + escAttr(child.value) + '"' + (child.disabled ? ' disabled' : '') + '>' + esc(child.textContent || child.value) + '</button>';
15112
16872
  }
15113
16873
  });
16874
+ // Snapshot refreshes commonly carry unchanged options. Rebuilding here
16875
+ // invalidates layout and detaches the moving carrier without a visual change.
16876
+ if (menu._newmarkSelectOptionsHtml === html) return;
16877
+ // Preserve the moving carrier across option markup rebuilds. Without this,
16878
+ // dispatching the native change event detaches the block before the delayed
16879
+ // color-block commit can finish, so users only see an instant snap.
16880
+ var colorBlock = menu._liquidColorBlock;
16881
+ if (colorBlock && colorBlock.parentNode === menu) colorBlock.remove();
15114
16882
  menu.innerHTML = html;
15115
- wireLiquidMenuInteractions(menu);
16883
+ menu._newmarkSelectOptionsHtml = html;
16884
+ if (colorBlock) menu.appendChild(colorBlock);
16885
+ wireDirectLiquidMenuInteractionsV2(menu);
15116
16886
  };
15117
16887
 
15118
16888
  window.enhanceNewmarkSelect = function(select) {
@@ -15153,7 +16923,7 @@ window.enhanceNewmarkSelect = function(select) {
15153
16923
  button.setAttribute('aria-label', String(accessibleLabel).trim());
15154
16924
  button.innerHTML = '<span class="newmark-select-button-label"></span>' + iconSvg('chevron-down', '', 'tiny');
15155
16925
  var menu = document.createElement('div');
15156
- menu.className = 'model-select-menu newmark-select-menu liquid-glass liquid-glass-carrier';
16926
+ menu.className = 'model-select-menu newmark-select-menu liquid-glass liquid-glass-carrier liquid-glass-popup';
15157
16927
  menu.setAttribute('role', 'listbox');
15158
16928
  menu.setAttribute('popover', 'manual');
15159
16929
  select.parentNode.insertBefore(shell, select);
@@ -15357,6 +17127,15 @@ function bindBackendQueuedRequestToTarget(text, target, occurrence) {
15357
17127
  request.backendManaged = true;
15358
17128
  request.provenance = 'backend-follow-up';
15359
17129
  request.backendOccurrence = Math.max(0, Number(occurrence || 0));
17130
+ request.queueItemId = queueItemIdForText(text, target, request);
17131
+ var queueItem = queueItemsForTarget(target).find(function(item) { return item.id === request.queueItemId; });
17132
+ if (queueItem) {
17133
+ request.requestedMode = queueItem.requestedMode;
17134
+ request.goalObjective = queueItem.goalObjective;
17135
+ request.createdAt = queueItem.createdAt;
17136
+ request.runId = queueItem.runId;
17137
+ request.images = (queueItem.images || []).map(function(image) { return Object.assign({}, image); });
17138
+ }
15360
17139
  return request;
15361
17140
  }
15362
17141
 
@@ -15422,27 +17201,14 @@ function rebindQueueToRuntimeBranch(target) {
15422
17201
  }
15423
17202
 
15424
17203
  function pauseQueueForTarget(target) {
15425
- var key = queueRuntimeKey(target);
15426
- if (!key) return false;
15427
- if (!state.queuePausedByTarget) state.queuePausedByTarget = {};
15428
- state.queuePausedByTarget[key] = true;
15429
- if (state.nextQueueSchedulesByTarget) delete state.nextQueueSchedulesByTarget[key];
15430
- if (state.nextQueueDrainsByTarget) delete state.nextQueueDrainsByTarget[key];
15431
- return true;
17204
+ return window.queueAction('queue_set_pause', { paused: true }, target);
15432
17205
  }
15433
17206
 
15434
17207
  window.toggleQueuePause = function() {
15435
17208
  var target = currentConversationTarget();
15436
17209
  var key = queueRuntimeKey(target);
15437
17210
  if (!key) return;
15438
- if (!state.queuePausedByTarget) state.queuePausedByTarget = {};
15439
- if (currentFlowRunning() && state.queuePausedByTarget[key]) {
15440
- window.stopFlowRun({ resumeQueue: true });
15441
- return;
15442
- }
15443
- state.queuePausedByTarget[key] = !state.queuePausedByTarget[key];
15444
- window.renderInputStack();
15445
- if (!state.queuePausedByTarget[key]) window.scheduleNextQueueDrain(target);
17211
+ return window.queueAction('queue_set_pause', { paused: !(state.queuePausedByTarget && state.queuePausedByTarget[key]) }, target);
15446
17212
  };
15447
17213
 
15448
17214
  function refreshNextPromptForTarget(target) {
@@ -15493,13 +17259,22 @@ function setQueueItemsForTarget(items, target) {
15493
17259
  goalObjective: String(item && item.goalObjective || ''),
15494
17260
  runId: String(item && item.runId || ''),
15495
17261
  createdAt: String(item && item.createdAt || ''),
17262
+ images: (Array.isArray(item && item.images) ? item.images : []).map(function(image) { return Object.assign({}, image); }),
15496
17263
  };
15497
17264
  });
15498
17265
  }
15499
17266
 
15500
- function queueItemIdForText(text, target) {
15501
- var items = queueItemsForTarget(target);
17267
+ function queueItemIdForText(text, target, request) {
17268
+ var items = queueItemsForTarget(target).filter(function(item) { return !item.queueMode || item.queueMode === 'followUp'; });
15502
17269
  var normalized = String(text || '');
17270
+ // Never redirect a removed/edited row to another item with equal text.
17271
+ if (request && request.queueItemId) {
17272
+ return items.some(function(item) { return item.id === request.queueItemId; }) ? request.queueItemId : '';
17273
+ }
17274
+ if (request && Number.isFinite(request.backendOccurrence)) {
17275
+ var occurrenceItem = items[request.backendOccurrence];
17276
+ return occurrenceItem && String(occurrenceItem.text || '') === normalized ? occurrenceItem.id : '';
17277
+ }
15503
17278
  for (var i = 0; i < items.length; i++) {
15504
17279
  if (String(items[i].text || '') === normalized) return items[i].id;
15505
17280
  }
@@ -15508,11 +17283,68 @@ function queueItemIdForText(text, target) {
15508
17283
 
15509
17284
  function syncQueueItemsFromSnapshot(snapshot, target) {
15510
17285
  if (snapshot && Array.isArray(snapshot.queueItems)) setQueueItemsForTarget(snapshot.queueItems, target);
17286
+ applyConversationCommandSnapshot(snapshot, target);
15511
17287
  }
15512
17288
 
15513
- window.queueAction = function(action, input) {
17289
+ function applyConversationCommandSnapshot(snapshot, target) {
17290
+ if (!snapshot || typeof snapshot !== 'object') return;
17291
+ target = normalizeQueuedConversationTarget(target || snapshot.target || currentConversationTarget());
17292
+ if (!target) return;
17293
+ var key = runtimeKeyFor(target.workspaceId, target.conversationId);
17294
+ if (typeof snapshot.queuePaused === 'boolean') {
17295
+ if (!state.queuePausedByTarget) state.queuePausedByTarget = {};
17296
+ state.queuePausedByTarget[key] = snapshot.queuePaused;
17297
+ }
17298
+ if (Array.isArray(snapshot.queueItems)) setQueueItemsForTarget(snapshot.queueItems, target);
17299
+ var queue = snapshot.queued && typeof snapshot.queued === 'object' ? snapshot.queued : snapshot.queue;
17300
+ if (queue && typeof queue === 'object') {
17301
+ setBackendQueueForTarget(queue, target);
17302
+ window.syncNextQueueFromBackend(queue, target);
17303
+ }
17304
+ if (Object.prototype.hasOwnProperty.call(snapshot, 'flowSuspension') || Object.prototype.hasOwnProperty.call(snapshot, 'flowRunning')) {
17305
+ var suspension = snapshot.flowSuspension;
17306
+ var flow = snapshot.flowRunning;
17307
+ var record = flowTakeoverRecordFor(target);
17308
+ var pendingStart = record.pendingStart && !record.pendingStart.cancelled ? record.pendingStart : null;
17309
+ record.running = !!(suspension || flow || pendingStart);
17310
+ record.paused = !!(suspension && suspension.reason === 'interrupted');
17311
+ record.name = String(suspension && suspension.workflowName || flow && flow.name || pendingStart && pendingStart.name || '');
17312
+ if (suspension && suspension.input !== undefined) record.promptText = String(suspension.input || '');
17313
+ if (isActiveConversationTarget(target) && window.renderFlowTakeover) window.renderFlowTakeover(record.running, record.name, { target: target, interrupted: record.paused, message: suspension && suspension.message });
17314
+ }
17315
+ if (!isActiveConversationTarget(target)) return;
17316
+ if (['build', 'chat', 'plan', 'goal', 'flow'].indexOf(snapshot.mode) >= 0) {
17317
+ state.mode = snapshot.mode;
17318
+ state._syncedMode = snapshot.mode;
17319
+ if (els['mode-select']) {
17320
+ els['mode-select'].value = snapshot.mode;
17321
+ window.syncNewmarkSelect(els['mode-select']);
17322
+ }
17323
+ var flowSelect = document.getElementById('flow-select');
17324
+ if (flowSelect) {
17325
+ flowSelect.dataset.newmarkVisible = snapshot.mode === 'flow' ? 'true' : 'false';
17326
+ window.syncNewmarkSelect(flowSelect);
17327
+ }
17328
+ }
17329
+ if (snapshot.inputMode === 'next' || snapshot.inputMode === 'guide') {
17330
+ window.setInputMode(snapshot.inputMode, false);
17331
+ state._syncedInputMode = snapshot.inputMode;
17332
+ }
17333
+ }
17334
+
17335
+ window.queueAction = function(action, input, target) {
15514
17336
  if (!api.queueAction) return Promise.resolve({ ok: false, error: 'queueAction unavailable' });
15515
- return api.queueAction(action, input, currentConversationTarget());
17337
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
17338
+ return Promise.resolve(api.queueAction(action, input, owner)).then(function(result) {
17339
+ applyConversationCommandSnapshot(result, owner);
17340
+ if (isActiveConversationTarget(owner)) window.renderInputStack();
17341
+ if (result && result.ok === false) showUiNotice(result.error || 'Queue action failed.', 'error', 'queue-' + action);
17342
+ return result;
17343
+ }).catch(function(error) {
17344
+ var message = error && error.message ? error.message : String(error);
17345
+ showUiNotice(message, 'error', 'queue-' + action);
17346
+ return { ok: false, error: message };
17347
+ });
15516
17348
  };
15517
17349
  function setBackendQueueForTarget(queue, target) {
15518
17350
  var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
@@ -15524,6 +17356,33 @@ function setBackendQueueForTarget(queue, target) {
15524
17356
  return normalized;
15525
17357
  }
15526
17358
 
17359
+ function queueRequestIndex(request) {
17360
+ if (!request) return -1;
17361
+ var requests = state.nextQueueRequests || [];
17362
+ if (!request.queueItemId) return requests.indexOf(request);
17363
+ return requests.findIndex(function(candidate) {
17364
+ return candidate && candidate.queueItemId === request.queueItemId && queuedRequestMatchesTarget(candidate, request.target);
17365
+ });
17366
+ }
17367
+
17368
+ window.queueIndexFromElement = function(element) {
17369
+ var row = element && element.closest('.queue-item');
17370
+ if (!row) return -1;
17371
+ var owner = { workspaceId: row.getAttribute('data-queue-workspace'), conversationId: row.getAttribute('data-queue-conversation') };
17372
+ if (!queuedRequestMatchesTarget({ target: owner }, currentConversationTarget())) return -1;
17373
+ var id = row.getAttribute('data-queue-id');
17374
+ return id ? queueRequestIndex({ target: owner, queueItemId: id }) : -1;
17375
+ };
17376
+
17377
+ window.refreshQueueRowIndexes = function() {
17378
+ // Background targets can change global array positions without changing
17379
+ // the visible rows. Retain their DOM/drag state and update only the index.
17380
+ document.querySelectorAll('#queue-list .queue-item[data-queue-id]').forEach(function(row) {
17381
+ var index = window.queueIndexFromElement(row);
17382
+ if (row.getAttribute('data-queue-index') !== String(index)) row.setAttribute('data-queue-index', String(index));
17383
+ });
17384
+ };
17385
+
15527
17386
  window.syncNextQueueFromBackend = function(queue, target) {
15528
17387
  target = normalizeQueuedConversationTarget(target || currentConversationTarget());
15529
17388
  if (!target) return;
@@ -15534,7 +17393,7 @@ window.syncNextQueueFromBackend = function(queue, target) {
15534
17393
  var retainedRequests = [];
15535
17394
  for (var index = 0; index < state.nextQueue.length; index++) {
15536
17395
  var existingRequest = state.nextQueueRequests[index];
15537
- if (queuedRequestIsBackendManaged(existingRequest) && queuedRequestMatchesTarget(existingRequest, target)) continue;
17396
+ if (queuedRequestMatchesTarget(existingRequest, target)) continue;
15538
17397
  retainedQueue.push(state.nextQueue[index]);
15539
17398
  retainedRequests.push(existingRequest || null);
15540
17399
  }
@@ -15548,6 +17407,11 @@ window.syncNextQueueFromBackend = function(queue, target) {
15548
17407
  state.nextQueueRequests.push(bindBackendQueuedRequestToTarget(text, target, occurrence));
15549
17408
  }
15550
17409
  refreshNextPromptForTarget(target);
17410
+ if (state.pendingInputEdit && state.pendingInputEdit.kind === 'queue') {
17411
+ state._editingQueueIndex = queueRequestIndex(state.pendingInputEdit.request);
17412
+ }
17413
+ if (state.queueDragRequest) state.queueDragIndex = queueRequestIndex(state.queueDragRequest);
17414
+ window.refreshQueueRowIndexes();
15551
17415
  };
15552
17416
 
15553
17417
  window.renderQueuePanel = function() {
@@ -15596,20 +17460,22 @@ window.renderQueuePanel = function() {
15596
17460
  // dev-0.5.5: a backend-managed item is editable/deletable when the kernel
15597
17461
  // exposed its stable queueItem id (mobile-originated queue entries now
15598
17462
  // carry ids through queueItems). Items without an id stay locked.
15599
- var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[queueIndex] || '', currentConversationTarget()) : '';
17463
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[queueIndex] || '', currentConversationTarget(), request) : '';
17464
+ var eventIndex = queueItemId ? 'window.queueIndexFromElement(this)' : String(queueIndex);
17465
+ var identityAttributes = queueItemId ? ' data-queue-id="' + escAttr(queueItemId) + '" data-queue-workspace="' + escAttr(target.workspaceId) + '" data-queue-conversation="' + escAttr(target.conversationId) + '"' : '';
15600
17466
  var operable = !backendManaged || !!queueItemId;
15601
17467
  var dragAttributes = operable
15602
- ? ' draggable="true" ondragstart="window.startQueueDrag(event,' + queueIndex + ')" ondragover="window.overQueueDrag(event,' + queueIndex + ')" ondrop="window.dropQueueDrag(event,' + queueIndex + ')" ondragend="window.endQueueDrag(event)"'
17468
+ ? ' draggable="true" ondragstart="window.startQueueDrag(event,' + eventIndex + ')" ondragover="window.overQueueDrag(event,' + eventIndex + ')" ondrop="window.dropQueueDrag(event,' + eventIndex + ')" ondragend="window.endQueueDrag(event)"'
15603
17469
  : ' draggable="false"';
15604
17470
  var dragHandle = operable
15605
17471
  ? '<button class="queue-drag" title="' + escAttr(t('queue.title')) + '">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</button>'
15606
17472
  : '<span class="queue-drag" aria-hidden="true">' + iconSvg('grip-vertical', t('queue.title'), 'tiny') + '</span>';
15607
17473
  var inputAttributes = ' readonly aria-readonly="true"';
15608
17474
  var actions = operable ?
15609
- '<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>' +
15610
- '<button class="stack-icon-btn" onclick="window.focusQueueItem(' + queueIndex + ')" title="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</button>' +
15611
- '<button class="stack-icon-btn danger" onclick="window.deleteQueueItem(' + queueIndex + ')" title="' + escAttr(t('common.delete')) + '">' + iconSvg('trash', t('common.delete'), 'tiny') + '</button>' : '';
15612
- html += '<div class="queue-item" data-queue-index="' + queueIndex + '"' + dragAttributes + '>' +
17475
+ '<button type="button" class="stack-icon-btn queue-guide-btn" onclick="window.guideQueueItem(' + eventIndex + ')" title="' + escAttr(t('queue.guideNow')) + '">' + iconSvg('corner-down-right', t('queue.guideNow'), 'tiny') + '<span>' + esc(t('queue.guideAction')) + '</span></button>' +
17476
+ '<button class="stack-icon-btn" onclick="window.focusQueueItem(' + eventIndex + ')" title="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</button>' +
17477
+ '<button class="stack-icon-btn danger" onclick="window.deleteQueueItem(' + eventIndex + ')" title="' + escAttr(t('common.delete')) + '">' + iconSvg('trash', t('common.delete'), 'tiny') + '</button>' : '';
17478
+ html += '<div class="queue-item" data-queue-index="' + queueIndex + '"' + identityAttributes + dragAttributes + '>' +
15613
17479
  dragHandle +
15614
17480
  '<input class="queue-edit" value="' + escAttr(state.nextQueue[queueIndex] || '') + '"' + inputAttributes + '>' +
15615
17481
  '<span class="stack-actions">' + actions + '</span>' +
@@ -15643,7 +17509,7 @@ window.focusQueueItem = function(idx) {
15643
17509
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
15644
17510
  if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15645
17511
  var backendManaged = queuedRequestIsBackendManaged(request);
15646
- if (backendManaged && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget())) return;
17512
+ if (backendManaged && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget(), request)) return;
15647
17513
  state._editingQueueIndex = idx;
15648
17514
  state.pendingInputEdit = { kind: 'queue', index: idx, request: request, target: currentConversationTarget(), originalText: String(state.nextQueue[idx] || '') };
15649
17515
  els.prompt.value = String(state.nextQueue[idx] || '');
@@ -15661,7 +17527,7 @@ window.deleteQueueItem = function(idx) {
15661
17527
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
15662
17528
  if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15663
17529
  var backendManaged = queuedRequestIsBackendManaged(request);
15664
- var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()) : '';
17530
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget(), request) : '';
15665
17531
  if (backendManaged && !queueItemId) return;
15666
17532
  if (!state.queueHiddenItems) state.queueHiddenItems = {};
15667
17533
  if (state.nextQueue[idx]) state.queueHiddenItems[queueHiddenItemKey(state.nextQueue[idx], currentConversationTarget())] = true;
@@ -15694,10 +17560,9 @@ window.guideQueueItem = function(idx) {
15694
17560
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
15695
17561
  if (!queuedRequestMatchesTarget(request, currentConversationTarget())) return;
15696
17562
  var backendManaged = queuedRequestIsBackendManaged(request);
15697
- var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()) : '';
15698
- if (backendManaged && !queueItemId) return;
17563
+ var queueItemId = backendManaged ? queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget(), request) : '';
17564
+ if (!backendManaged || !queueItemId) return;
15699
17565
  var target = normalizeQueuedConversationTarget(request.target) || currentConversationTarget();
15700
- var deliverDuringRun = isCurrentConversationRunning();
15701
17566
  var next = state.nextQueue.splice(idx, 1)[0];
15702
17567
  var nextRequest = state.nextQueueRequests ? state.nextQueueRequests.splice(idx, 1)[0] : null;
15703
17568
  if (!state.queueHiddenItems) state.queueHiddenItems = {};
@@ -15707,7 +17572,7 @@ window.guideQueueItem = function(idx) {
15707
17572
  if (backendManaged) {
15708
17573
  // The kernel owns this entry; guide it there so the mobile side stays
15709
17574
  // in sync. On failure restore the local row.
15710
- return window.queueAction('queue_guide', { id: queueItemId }).then(function(outcome) {
17575
+ return window.queueAction('queue_guide', { id: queueItemId }, target).then(function(outcome) {
15711
17576
  if (!outcome || outcome.ok === false) {
15712
17577
  restoreQueueItemAfterGuideFailure(idx, next, nextRequest, target, true);
15713
17578
  showUiNotice(outcome && outcome.error ? outcome.error : (currentLang() === 'zh' ? 'Guide 投递失败' : 'Guide delivery failed'), 'error', 'queue-guide-' + queueItemId);
@@ -15718,39 +17583,21 @@ window.guideQueueItem = function(idx) {
15718
17583
  return { ok: false, error: error && error.message ? error.message : String(error) };
15719
17584
  });
15720
17585
  }
15721
- if (next) {
15722
- var delivery = window.sendMessage('guide', next, {
15723
- fromQueue: true,
15724
- forceBuild: true,
15725
- deliverAsGuide: deliverDuringRun,
15726
- queuedRequest: nextRequest
15727
- });
15728
- if (deliverDuringRun && delivery && typeof delivery.then === 'function') {
15729
- return Promise.resolve(delivery).then(function(outcome) {
15730
- if (outcome && outcome.ok === false) {
15731
- restoreQueueItemAfterGuideFailure(idx, next, nextRequest, target, !outcome.guideReceipt);
15732
- }
15733
- return outcome;
15734
- }, function(error) {
15735
- restoreQueueItemAfterGuideFailure(idx, next, nextRequest, target, true);
15736
- return { ok: false, error: error && error.message ? error.message : String(error) };
15737
- });
15738
- }
15739
- return delivery;
15740
- }
15741
17586
  };
15742
17587
 
15743
17588
  window.startQueueDrag = function(event, idx) {
17589
+ state.queueDragRequest = null;
15744
17590
  var request = state.nextQueueRequests && state.nextQueueRequests[idx];
15745
17591
  if (!queuedRequestMatchesTarget(request, currentConversationTarget())) {
15746
17592
  state.queueDragIndex = -1;
15747
17593
  return;
15748
17594
  }
15749
- if (queuedRequestIsBackendManaged(request) && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget())) {
17595
+ if (queuedRequestIsBackendManaged(request) && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget(), request)) {
15750
17596
  state.queueDragIndex = -1;
15751
17597
  return;
15752
17598
  }
15753
17599
  state.queueDragIndex = idx;
17600
+ state.queueDragRequest = request;
15754
17601
  state.queueDragTargetIndex = idx;
15755
17602
  if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
15756
17603
  var row = event.currentTarget;
@@ -15784,12 +17631,14 @@ function previewQueueDrag(targetIndex) {
15784
17631
 
15785
17632
  window.overQueueDrag = function(event, idx) {
15786
17633
  event.preventDefault();
17634
+ if (idx < 0) return;
15787
17635
  if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
15788
17636
  if (state.queueDragIndex >= 0) previewQueueDrag(queueDropTargetIndex(event, idx));
15789
17637
  };
15790
17638
 
15791
17639
  window.dropQueueDrag = function(event, idx) {
15792
17640
  event.preventDefault();
17641
+ if (idx < 0) return;
15793
17642
  var from = state.queueDragIndex;
15794
17643
  idx = typeof queueDropTargetIndex === 'function' ? queueDropTargetIndex(event, idx) : idx;
15795
17644
  if (!state.nextQueue || from < 0 || from === idx || from >= state.nextQueue.length) return;
@@ -15799,30 +17648,35 @@ window.dropQueueDrag = function(event, idx) {
15799
17648
  || !queuedRequestMatchesTarget(destinationRequest, currentConversationTarget())) return;
15800
17649
  var sourceBackend = queuedRequestIsBackendManaged(sourceRequest);
15801
17650
  var destinationBackend = queuedRequestIsBackendManaged(destinationRequest);
15802
- if ((sourceBackend && !queueItemIdForText(state.nextQueue[from] || '', currentConversationTarget()))
15803
- || (destinationBackend && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget()))) return;
17651
+ if ((sourceBackend && !queueItemIdForText(state.nextQueue[from] || '', currentConversationTarget(), sourceRequest))
17652
+ || (destinationBackend && !queueItemIdForText(state.nextQueue[idx] || '', currentConversationTarget(), destinationRequest))) return;
15804
17653
  var item = state.nextQueue.splice(from, 1)[0];
15805
17654
  var requestItem = state.nextQueueRequests ? state.nextQueueRequests.splice(from, 1)[0] : null;
15806
17655
  state.nextQueue.splice(idx, 0, item);
15807
17656
  if (state.nextQueueRequests) state.nextQueueRequests.splice(idx, 0, requestItem || null);
15808
17657
  state.queueDragIndex = -1;
17658
+ state.queueDragRequest = null;
15809
17659
  refreshNextPromptForTarget(currentConversationTarget());
15810
- window.renderInputStack();
15811
17660
  if (sourceBackend || destinationBackend) {
15812
17661
  // The kernel owns at least one affected entry: forward the complete
15813
17662
  // ordered id list so mobile and desktop stay in sync.
15814
17663
  var visibleIndexes = queueIndexesForTarget(currentConversationTarget());
15815
17664
  var orderedIds = visibleIndexes.map(function(vi) {
15816
- return queueItemIdForText(state.nextQueue[vi] || '', currentConversationTarget());
17665
+ var queuedRequest = state.nextQueueRequests && state.nextQueueRequests[vi];
17666
+ return queuedRequestIsBackendManaged(queuedRequest) ? queueItemIdForText(state.nextQueue[vi] || '', currentConversationTarget(), queuedRequest) : '';
15817
17667
  }).filter(function(id) { return !!id; });
15818
17668
  if (orderedIds.length) {
15819
17669
  window.queueAction('queue_reorder', { orderedIds: orderedIds });
15820
17670
  }
15821
17671
  }
17672
+ // Rendering reloads the last backend snapshot. Submit dragged identities
17673
+ // first so that reconciliation cannot undo the requested order.
17674
+ window.renderInputStack();
15822
17675
  };
15823
17676
 
15824
17677
  window.endQueueDrag = function(event) {
15825
17678
  state.queueDragIndex = -1;
17679
+ state.queueDragRequest = null;
15826
17680
  state.queueDragTargetIndex = -1;
15827
17681
  document.querySelectorAll('#queue-list .queue-item').forEach(function(row) {
15828
17682
  row.classList.remove('queue-drag-displaced', 'dragging');
@@ -15843,8 +17697,9 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
15843
17697
  opts = opts || {};
15844
17698
  var requestedMode = opts.requestedMode || state.mode || 'build';
15845
17699
  var effectiveInputMode = modeOverride || state.inputMode || 'guide';
15846
- if (effectiveInputMode === 'guide' && !isCurrentConversationRunning()) effectiveInputMode = 'next';
15847
- if (requestedMode === 'flow') {
17700
+ // Preserve the selected delivery mode. The shared owner decides whether
17701
+ // Guide steers a live run or starts a new instruction when the target is idle.
17702
+ if (requestedMode === 'flow' && !state.pendingInputEdit) {
15848
17703
  effectiveInputMode = 'guide';
15849
17704
  if (!opts.fromQueue) {
15850
17705
  if (currentFlowRunning()) {
@@ -15855,9 +17710,13 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
15855
17710
  els.prompt.dispatchEvent(new Event('input', { bubbles: true }));
15856
17711
  }
15857
17712
  if (state.inputMode === 'next') return window.queueFlowNextInput(activeFlowGuide);
15858
- return api.guideFlow ? api.guideFlow(activeFlowGuide, currentConversationTarget()) : { ok: false, error: 'Flow Guide API unavailable' };
17713
+ var activeFlowTarget = opts.target || currentConversationTarget();
17714
+ return api.sendMessage(activeFlowGuide, activeFlowTarget, { requestedMode: requestedMode, inputMode: 'guide', clientMessageId: window.crypto.randomUUID() }).then(function(result) {
17715
+ applyConversationCommandSnapshot(result, activeFlowTarget);
17716
+ return result;
17717
+ });
15859
17718
  }
15860
- return window.submitSelectedFlow();
17719
+ return window.submitSelectedFlow({ text: typeof queuedText === 'string' ? queuedText : String(els.prompt.value || ''), target: opts.target || currentConversationTarget(), inputMode: modeOverride || state.inputMode });
15861
17720
  }
15862
17721
  }
15863
17722
  if (state.pendingInputEdit && typeof queuedText !== 'string' && !opts.fromQueue) {
@@ -15875,36 +17734,18 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
15875
17734
  var editIndex = Number(pendingEdit.index);
15876
17735
  var editRequest = pendingEdit.request || (state.nextQueueRequests && state.nextQueueRequests[editIndex]);
15877
17736
  var editBackendManaged = queuedRequestIsBackendManaged(editRequest);
15878
- var editQueueItemId = editBackendManaged ? queueItemIdForText(String(state.nextQueue[editIndex] || pendingEdit.originalText || ''), pendingEdit.target) : '';
17737
+ var editQueueItemId = editBackendManaged ? queueItemIdForText(String(pendingEdit.originalText || ''), pendingEdit.target, editRequest) : '';
15879
17738
  if (editBackendManaged && !editQueueItemId) {
15880
17739
  showUiNotice(currentLang() === 'zh' ? '无法编辑该队列项:缺少后端 id' : 'Cannot edit queue item: backend id missing', 'error', 'queue-edit-' + editIndex);
15881
17740
  return { ok: false, error: 'missing backend queue id' };
15882
17741
  }
15883
- if (effectiveInputMode === 'next') {
15884
- if (editBackendManaged) {
15885
- // The kernel owns this entry: forward the edit and let the next
15886
- // snapshot reconcile the local row.
15887
- window.queueAction('queue_update', { id: editQueueItemId, text: editedValue, requestedMode: String(editRequest && editRequest.requestedMode || 'build'), goalObjective: String(editRequest && editRequest.goalObjective || ''), createdAt: String(editRequest && editRequest.createdAt || '') });
15888
- state.nextQueue[editIndex] = editedValue;
15889
- if (state.nextQueueRequests[editIndex]) state.nextQueueRequests[editIndex].text = editedValue;
15890
- refreshNextPromptForTarget(pendingEdit.target);
15891
- window.renderInputStack();
15892
- return { ok: true, queued: true };
15893
- }
15894
- state.nextQueue[editIndex] = editedValue;
15895
- if (state.nextQueueRequests[editIndex]) {
15896
- state.nextQueueRequests[editIndex].text = editedValue;
15897
- state.nextQueueRequests[editIndex].images = editedImages;
15898
- }
15899
- refreshNextPromptForTarget(pendingEdit.target);
15900
- window.renderInputStack();
15901
- return { ok: true, queued: true };
15902
- }
15903
- state.nextQueue.splice(editIndex, 1);
15904
- var editedRequest = state.nextQueueRequests.splice(editIndex, 1)[0] || pendingEdit.request;
15905
- if (editedRequest) { editedRequest.text = editedValue; editedRequest.images = editedImages; }
15906
- window.renderInputStack();
15907
- return window.sendMessage('guide', editedValue, { fromQueue: true, forceBuild: true, deliverAsGuide: isCurrentConversationRunning(), queuedRequest: editedRequest });
17742
+ if (!editBackendManaged) return { ok: false, error: 'Queue entry has no shared owner' };
17743
+ return window.queueAction(effectiveInputMode === 'next' ? 'queue_update' : 'queue_guide', {
17744
+ id: editQueueItemId, text: editedValue,
17745
+ requestedMode: String(editRequest.requestedMode || 'build'),
17746
+ goalObjective: String(editRequest.goalObjective || ''),
17747
+ createdAt: String(editRequest.createdAt || ''), images: editedImages
17748
+ }, pendingEdit.target);
15908
17749
  }
15909
17750
  if (pendingEdit.kind === 'goal') {
15910
17751
  var previousGoalEditMode = pendingEdit.previousMode || 'goal';
@@ -15926,19 +17767,11 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
15926
17767
  restoreModeAfterGoalEdit(previousGoalEditMode);
15927
17768
  });
15928
17769
  }
15929
- if (!state.nextQueue) state.nextQueue = [];
15930
- if (!state.nextQueueRequests) state.nextQueueRequests = [];
15931
- state.nextQueue.push(editedValue);
15932
- var goalRequest = bindQueuedRequestToTarget({ text: editedValue, images: editedImages }, editedValue, currentConversationTarget(), queueBranchPathForTarget(currentConversationTarget(), 'runtime'));
15933
- goalRequest.goalDeclaration = true;
15934
- goalRequest.goalObjective = editedValue;
15935
- goalRequest.requestedMode = 'goal';
15936
- state.nextQueueRequests.push(goalRequest);
15937
17770
  state.queueCollapsed = false;
15938
- refreshNextPromptForTarget(currentConversationTarget());
15939
- window.renderInputStack();
15940
- restoreModeAfterGoalEdit(previousGoalEditMode);
15941
- return { ok: true, queued: true, goal: editedValue };
17771
+ return window.queueAction('queue_enqueue', {
17772
+ id: window.crypto.randomUUID(), text: editedValue, images: editedImages,
17773
+ requestedMode: 'goal', goalObjective: editedValue, createdAt: new Date().toISOString()
17774
+ }, currentConversationTarget()).finally(function() { restoreModeAfterGoalEdit(previousGoalEditMode); });
15942
17775
  }
15943
17776
  }
15944
17777
  var queuedRequestRecord = opts.queuedRequest && typeof opts.queuedRequest === 'object' ? opts.queuedRequest : null;
@@ -15978,8 +17811,8 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
15978
17811
  displayText = continuePhrase;
15979
17812
  }
15980
17813
 
15981
- var lockedConversationId = activeConversationId();
15982
- var lockedTarget = currentConversationTarget(lockedConversationId);
17814
+ var lockedTarget = normalizeQueuedConversationTarget(opts.target || currentConversationTarget());
17815
+ var lockedConversationId = lockedTarget.conversationId;
15983
17816
  var lockedRuntimeKey = runtimeKeyFor(lockedTarget.workspaceId, lockedTarget.conversationId);
15984
17817
  var activeRuntime = runningConversationRecord(lockedConversationId, lockedTarget.workspaceId);
15985
17818
  var conversationRunning = !!activeRuntime;
@@ -15991,7 +17824,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
15991
17824
  if (reconciledLocalRuntime) {
15992
17825
  activeRuntime = runningConversationRecord(lockedConversationId, lockedTarget.workspaceId);
15993
17826
  conversationRunning = !!activeRuntime;
15994
- if (!conversationRunning && effectiveInputMode === 'guide') effectiveInputMode = 'next';
15995
17827
  }
15996
17828
  }
15997
17829
 
@@ -16015,13 +17847,11 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16015
17847
  queuedModeRequest.goalDeclaration = true;
16016
17848
  queuedModeRequest.goalObjective = rawText;
16017
17849
  }
16018
- var queueResult = await window.queueAction('queue_enqueue', {
16019
- id: queuedModeRequest.clientMessageId,
16020
- text: rawText,
17850
+ var queueResult = await api.sendMessage({ text: rawText, images: queuedModeRequest.images }, lockedTarget, {
17851
+ clientMessageId: queuedModeRequest.clientMessageId,
17852
+ inputMode: 'next',
16021
17853
  requestedMode: requestedMode,
16022
- goalObjective: requestedMode === 'goal' ? rawText : '',
16023
- images: queuedModeRequest.images,
16024
- createdAt: queuedModeRequest.createdAt || new Date().toISOString()
17854
+ goalObjective: requestedMode === 'goal' ? rawText : ''
16025
17855
  });
16026
17856
  if (!queueResult || queueResult.ok === false) {
16027
17857
  showUiNotice(queueResult && queueResult.error ? queueResult.error : (currentLang() === 'zh' ? '消息加入队列失败' : 'Failed to queue message'), 'error', 'queue-enqueue-' + queuedModeRequest.clientMessageId);
@@ -16029,6 +17859,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16029
17859
  }
16030
17860
  if (queueResult.queued) setBackendQueueForTarget(queueResult.queued, lockedTarget);
16031
17861
  if (Array.isArray(queueResult.queueItems)) setQueueItemsForTarget(queueResult.queueItems, lockedTarget);
17862
+ applyConversationCommandSnapshot(queueResult, lockedTarget);
16032
17863
  window.syncNextQueueFromBackend(queueResult.queued || backendQueueForTarget(lockedTarget), lockedTarget);
16033
17864
  els.prompt.value = '';
16034
17865
  clearActiveConversationDraft();
@@ -16086,8 +17917,19 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16086
17917
  allowStatusReset: retryingUnacknowledgedGuide
16087
17918
  }, lockedTarget);
16088
17919
  renderPendingGuideMessages(lockedTarget, {});
16089
- var enqueue = api.enqueueGuide ? api.enqueueGuide(envelope) : api.sendMessage(requestMessage, lockedConversationId);
16090
- return Promise.resolve(enqueue).then(function(receipt) {
17920
+ var enqueue = api.sendMessage(Object.assign({}, envelope, { visibleMode: requestedMode }), lockedTarget, {
17921
+ requestedMode: requestedMode, inputMode: 'guide', goalObjective: envelope.goalObjective,
17922
+ clientMessageId: clientMessageId
17923
+ });
17924
+ return Promise.resolve(enqueue).then(function(result) {
17925
+ applyConversationCommandSnapshot(result, lockedTarget);
17926
+ var receipt = result && (result.receipt || result.guideReceipt) || result;
17927
+ if (result && !result.receipt && !result.guideReceipt && ['completed', 'interrupted', 'idle', 'error'].indexOf(result.status) >= 0) {
17928
+ // A stale renderer may have displayed a run which the shared owner
17929
+ // already finalized. That same command now owns its fresh response.
17930
+ if (isActiveConversationTarget(lockedTarget) && Array.isArray(result.chatMessages)) renderChatMessages(result.chatMessages, lockedTarget);
17931
+ return result;
17932
+ }
16091
17933
  if (!receipt || typeof receipt !== 'object' || !String(receipt.status || '').trim()) {
16092
17934
  throw new Error(currentLang() === 'zh' ? '未收到有效的 Guide 回执' : 'A valid Guide acknowledgement was not received');
16093
17935
  }
@@ -16202,7 +18044,12 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16202
18044
  updateSubmitButtonState();
16203
18045
  var executionMode = opts.goalDeclaration || requestedMode === 'goal' ? 'build'
16204
18046
  : ((opts.forceBuild || idleBuildNextImmediate) && requestedMode === 'build' ? 'build' : requestedMode);
16205
- await syncConversationExecutionState(executionMode, effectiveInputMode);
18047
+ // The command freezes its mode and delivery policy; selector persistence
18048
+ // must not rewrite the mode of an already running target before acceptance.
18049
+ if (api.setModel && state.model !== state._syncedModel) {
18050
+ var selectedModelResult = await api.setModel(state.model);
18051
+ if (selectedModelResult != null) state._syncedModel = state.model;
18052
+ }
16206
18053
  if (consumeProvisionalStop(lockedTarget, localRunId)) {
16207
18054
  // Stop/Escape won before the backend accepted the prompt. Do not send the
16208
18055
  // provider request after the user has already cancelled it.
@@ -16242,14 +18089,21 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16242
18089
  }
16243
18090
  var responseMsg = null;
16244
18091
  var sendFailure = '';
18092
+ var sendLoadGeneration = state.conversationLoadGeneration;
16245
18093
  try {
16246
18094
  var activationBeforeSend = pendingConversationActivation(lockedTarget);
16247
18095
  if (activationBeforeSend) await activationBeforeSend;
16248
- var sendPromise = api.sendMessage(requestMessage, lockedTarget);
18096
+ var sendPromise = api.sendMessage(requestMessage, lockedTarget, {
18097
+ requestedMode: requestedMode, inputMode: effectiveInputMode,
18098
+ goalObjective: opts.goalObjective || '',
18099
+ clientMessageId: String(requestMessage && requestMessage.clientMessageId || localRunId)
18100
+ });
16249
18101
  if (requestedMode === 'goal') activateSubmittedGoal(opts.goalObjective || rawText);
16250
18102
  var r = await sendPromise;
18103
+ var sendResultOwnsRuntime = conversationRunStillOwnsTarget(lockedTarget, r && r.runId, localRunId);
18104
+ if (sendResultOwnsRuntime) applyConversationCommandSnapshot(r, lockedTarget);
16251
18105
  if (r && r.error) sendFailure = String(r.error);
16252
- if (isActiveConversationTarget(lockedTarget)) applyReturnedGoalState(r);
18106
+ if (sendResultOwnsRuntime && isActiveConversationTarget(lockedTarget)) applyReturnedGoalState(r);
16253
18107
  if (r && r.runId) {
16254
18108
  var anchorStore = workRunAnchorIndexStore(lockedTarget);
16255
18109
  if (anchorStore[localRunId] !== undefined) {
@@ -16258,8 +18112,13 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16258
18112
  }
16259
18113
  }
16260
18114
  if (r && r.runtimeKey) registerRuntimeKey(lockedTarget, r.runtimeKey);
16261
- if (r && r.runId) setConversationRuntimeState(lockedTarget, r.status || 'running', r.runId, { provisional: false, generation: r.generation || 0, runtimeKey: r.runtimeKey || '' });
16262
- var stillActive = isActiveConversationTarget(lockedTarget) && isViewingRuntimeConversationBranch(lockedTarget);
18115
+ if (sendResultOwnsRuntime && r && r.runId) {
18116
+ var observedSendRuntime = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(lockedTarget.workspaceId, lockedTarget.conversationId)];
18117
+ var receivedSendStatus = observedSendRuntime && observedSendRuntime.runId === r.runId && ['interrupted', 'force_interrupted', 'error'].indexOf(observedSendRuntime.status) >= 0
18118
+ ? observedSendRuntime.status : (r.status || 'running');
18119
+ setConversationRuntimeState(lockedTarget, receivedSendStatus, r.runId, { provisional: false, generation: r.generation || 0, runtimeKey: r.runtimeKey || '' });
18120
+ }
18121
+ var stillActive = sendResultOwnsRuntime && sendLoadGeneration === state.conversationLoadGeneration && isActiveConversationTarget(lockedTarget) && isViewingRuntimeConversationBranch(lockedTarget);
16263
18122
  if (stillActive) applyAutoRouteRatingState(r);
16264
18123
  if (stillActive && r && Array.isArray(r.workRuns)) syncWorkRunsSnapshot(r.workRuns, lockedTarget);
16265
18124
  var responseOwnedByRun = !!(r && Array.isArray(r.workRuns) && r.workRuns.some(function(run) {
@@ -16273,7 +18132,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16273
18132
  };
16274
18133
  upsertWorkReviewRecord(lockedUi, reviewRecord.diffs, reviewRecord.runId, [localRunId]);
16275
18134
  }
16276
- if (stillActive && !responseOwnedByRun) {
18135
+ if (stillActive && !responseOwnedByRun && !(r && (r.receipt || r.guideReceipt || (r.runtime && r.runtime.running)))) {
16277
18136
  responseMsg = lockedUi.activeWorkflowMsg;
16278
18137
  if (r && r.error) {
16279
18138
  if (!responseMsg) responseMsg = addMsg('assistant', '', state.mode, state.model);
@@ -16308,7 +18167,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16308
18167
  addWorkReview(item.diffs || item, item.runId || '', [localRunId]);
16309
18168
  });
16310
18169
  }
16311
- if (r && r.conversations) {
18170
+ if (sendResultOwnsRuntime && r && r.conversations) {
16312
18171
  applyBackendConversations(r.conversations || [], stillActive ? lockedConversationId : activeConversationId(), lockedTarget.workspaceId);
16313
18172
  if (stillActive) renderConversations();
16314
18173
  }
@@ -16324,7 +18183,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16324
18183
  state.subagents = r.subagents;
16325
18184
  if (state.rightTab === 'subagent') window.renderSubagentList();
16326
18185
  }
16327
- if (r && r.queued) {
18186
+ if (sendResultOwnsRuntime && r && r.queued) {
16328
18187
  var responseQueue = setBackendQueueForTarget(r.queued, lockedTarget);
16329
18188
  window.syncNextQueueFromBackend(responseQueue, lockedTarget);
16330
18189
  syncQueueItemsFromSnapshot(r, lockedTarget);
@@ -16339,23 +18198,20 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16339
18198
  }
16340
18199
  } catch(e) {
16341
18200
  sendFailure = e && e.message ? String(e.message) : String(e || 'Agent run failed.');
16342
- if (isActiveConversationTarget(lockedTarget)) {
18201
+ if (conversationRunStillOwnsTarget(lockedTarget, localRunId) && sendLoadGeneration === state.conversationLoadGeneration && isActiveConversationTarget(lockedTarget)) {
16343
18202
  responseMsg = conversationWorkUiState(lockedConversationId, lockedTarget.workspaceId).activeWorkflowMsg || addMsg('assistant', '', state.mode, state.model);
16344
18203
  updateMsg(responseMsg, formatChatError(sendFailure, 'Agent run failed.'), 'error', state.mode, state.model);
16345
18204
  }
16346
18205
  }
16347
18206
  if (api.getState) {
18207
+ var completedContextRequest = captureContextWindowSnapshotRequest(lockedTarget);
18208
+ var completedWorkRequest = captureConversationWorkSnapshotRequest(lockedTarget);
16348
18209
  api.getState(lockedTarget).then(function(s) {
18210
+ if (!isCurrentConversationWorkSnapshotRequest(completedWorkRequest)) return;
16349
18211
  if (s && s.runtimeKey) registerRuntimeKey(lockedTarget, s.runtimeKey);
16350
18212
  var stillActiveAfterRefresh = isActiveConversationTarget(lockedTarget) && isViewingRuntimeConversationBranch(lockedTarget);
16351
18213
  if (stillActiveAfterRefresh) applyReturnedGoalState(s);
16352
- if (stillActiveAfterRefresh && s && s.contextCompression !== undefined) {
16353
- state.contextCompression = s.contextCompression;
16354
- }
16355
- if (stillActiveAfterRefresh && s && s.contextWindow !== undefined) {
16356
- state.contextWindow = s.contextWindow;
16357
- window.renderContextWindow();
16358
- }
18214
+ if (stillActiveAfterRefresh) applyContextWindowSnapshot(s, completedContextRequest);
16359
18215
  if (stillActiveAfterRefresh && s && s.subagents) state.subagents = s.subagents;
16360
18216
  if (stillActiveAfterRefresh && s && s.linkedPlan) state.linkedPlan = s.linkedPlan;
16361
18217
  if (stillActiveAfterRefresh && s && s.fileDiffs) state.fileDiffs = s.fileDiffs;
@@ -16388,10 +18244,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16388
18244
  }
16389
18245
  }).catch(function(){});
16390
18246
  }
16391
- if (api.setMode && executionMode !== state.mode) {
16392
- var resumedModeResult = await api.setMode(state.mode).catch(function() { return null; });
16393
- if (resumedModeResult != null) state._syncedMode = state.mode;
16394
- }
16395
18247
  var finalRuntime = state.conversationRuntimeStates && state.conversationRuntimeStates[lockedRuntimeKey];
16396
18248
  var finalRunMatches = !finalRuntime || !finalRuntime.runId || finalRuntime.runId === localRunId || (r && finalRuntime.runId === r.runId);
16397
18249
  if (sendFailure) {
@@ -16401,9 +18253,16 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16401
18253
  if (finalRunMatches) {
16402
18254
  setConversationRuntimeState(lockedTarget, 'error', (r && r.runId) || (finalRuntime && finalRuntime.runId) || localRunId, { provisional: false });
16403
18255
  }
18256
+ } else if (finalRunMatches && r && r.runtime && (r.runtime.running || r.runtime.stopRequested)) {
18257
+ setConversationRuntimeState(lockedTarget, r.runtime.stopRequested ? 'stopping' : 'running', r.runtime.runId || r.runId || localRunId, { provisional: false });
16404
18258
  } else if (finalRunMatches) {
16405
- setConversationRuntimeState(lockedTarget, 'completed', (r && r.runId) || localRunId);
18259
+ var settledStatus = finalRuntime && ['interrupted', 'force_interrupted', 'error'].indexOf(finalRuntime.status) >= 0
18260
+ ? finalRuntime.status : ((r && ['interrupted', 'force_interrupted'].indexOf(r.status) >= 0) ? r.status : 'completed');
18261
+ setConversationRuntimeState(lockedTarget, settledStatus, (r && r.runId) || localRunId);
16406
18262
  }
18263
+ // The synchronous final status above belongs to this same send completion.
18264
+ // A later IPC event still advances the revision and invalidates the read.
18265
+ if (completedWorkRequest) completedWorkRequest.revision = conversationWorkRevision(lockedTarget);
16407
18266
  setWorking(!!runningConversationRecord(activeConversationId()));
16408
18267
  window.renderInputStack();
16409
18268
  var canonicalLockedRuntimeKey = runtimeKeyFor(lockedTarget.workspaceId, lockedTarget.conversationId);
@@ -16416,107 +18275,14 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16416
18275
  window.drainNextQueue();
16417
18276
  };
16418
18277
 
16419
- window.scheduleNextQueueDrain = function(target) {
16420
- var expectedTarget = normalizeQueuedConversationTarget(target || currentConversationTarget());
16421
- if (!expectedTarget) return;
16422
- if (state.queuePausedByTarget && state.queuePausedByTarget[runtimeKeyFor(expectedTarget.workspaceId, expectedTarget.conversationId)]) return;
16423
- var expectedKey = runtimeKeyFor(expectedTarget.workspaceId, expectedTarget.conversationId);
16424
- if (!state.nextQueueSchedulesByTarget) state.nextQueueSchedulesByTarget = {};
16425
- var existing = state.nextQueueSchedulesByTarget[expectedKey];
16426
- if (existing) return;
16427
- var schedule = { target: expectedTarget };
16428
- state.nextQueueSchedulesByTarget[expectedKey] = schedule;
16429
- queueMicrotask(function() {
16430
- if (state.nextQueueSchedulesByTarget[expectedKey] !== schedule) return;
16431
- var activeTarget = currentConversationTarget();
16432
- if (runtimeKeyFor(activeTarget.workspaceId, activeTarget.conversationId) !== expectedKey) {
16433
- delete state.nextQueueSchedulesByTarget[expectedKey];
16434
- return;
16435
- }
16436
- if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[expectedKey]) {
16437
- delete state.nextQueueSchedulesByTarget[expectedKey];
16438
- return;
16439
- }
16440
- if (state.queuePausedByTarget && state.queuePausedByTarget[runtimeKeyFor(expectedTarget.workspaceId, expectedTarget.conversationId)]) {
16441
- delete state.nextQueueSchedulesByTarget[expectedKey];
16442
- return;
16443
- }
16444
- if (isCurrentConversationRunning()) {
16445
- delete state.nextQueueSchedulesByTarget[expectedKey];
16446
- return;
16447
- }
16448
- window.drainNextQueue();
16449
- if (state.nextQueueSchedulesByTarget[expectedKey] === schedule) {
16450
- delete state.nextQueueSchedulesByTarget[expectedKey];
16451
- }
16452
- });
18278
+ window.scheduleNextQueueDrain = function() {
18279
+ // Queue consumption belongs to the shared conversation command owner.
18280
+ // This compatibility hook is presentation-only; it cannot schedule a send.
16453
18281
  };
16454
18282
 
16455
18283
  window.drainNextQueue = function() {
16456
- if (isCurrentConversationRunning()) return;
16457
- if (!state.nextQueue || !state.nextQueue.length) return;
16458
- var target = currentConversationTarget();
16459
- if (state.queuePausedByTarget && state.queuePausedByTarget[runtimeKeyFor(target.workspaceId, target.conversationId)]) return;
16460
- var targetKey = runtimeKeyFor(target.workspaceId, target.conversationId);
16461
- if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[targetKey]) return;
16462
- if (!state.nextQueueDrainsByTarget) state.nextQueueDrainsByTarget = {};
16463
- if (state.nextQueueDrainsByTarget[targetKey]) return;
16464
- var indexes = queueIndexesForRuntimeTarget(target).filter(function(index) {
16465
- return !queuedRequestIsBackendManaged(state.nextQueueRequests && state.nextQueueRequests[index]);
16466
- });
16467
- if (!indexes.length) return;
16468
- var index = indexes[0];
16469
- var nextRequest = state.nextQueueRequests ? state.nextQueueRequests[index] : null;
16470
- if (!queuedRequestMatchesTarget(nextRequest, target)) return;
16471
- var drainToken = { request: nextRequest, target: target };
16472
- state.nextQueueDrainsByTarget[targetKey] = drainToken;
16473
- queueMicrotask(function() {
16474
- if (state.nextQueueDrainsByTarget[targetKey] !== drainToken) return;
16475
- var activeTarget = currentConversationTarget();
16476
- if (runtimeKeyFor(activeTarget.workspaceId, activeTarget.conversationId) !== targetKey
16477
- || (state.activeSendCallsByTarget && state.activeSendCallsByTarget[targetKey])
16478
- || isCurrentConversationRunning()) {
16479
- delete state.nextQueueDrainsByTarget[targetKey];
16480
- return;
16481
- }
16482
- var currentIndex = state.nextQueueRequests ? state.nextQueueRequests.indexOf(nextRequest) : -1;
16483
- if (currentIndex < 0 || queuedRequestIsBackendManaged(nextRequest) || !queuedRequestMatchesTarget(nextRequest, activeTarget)) {
16484
- delete state.nextQueueDrainsByTarget[targetKey];
16485
- return;
16486
- }
16487
- var next = state.nextQueue[currentIndex];
16488
- state.nextQueue.splice(currentIndex, 1);
16489
- state.nextQueueRequests.splice(currentIndex, 1);
16490
- refreshNextPromptForTarget(activeTarget);
16491
- window.renderInputStack();
16492
- var started = false;
16493
- var sendAttempt = window.sendMessage('guide', next, {
16494
- fromQueue: true,
16495
- forceBuild: !nextRequest.goalDeclaration,
16496
- goalDeclaration: !!nextRequest.goalDeclaration,
16497
- goalObjective: next,
16498
- requestedMode: nextRequest.requestedMode || (nextRequest.goalDeclaration ? 'goal' : state.mode),
16499
- queuedRequest: nextRequest,
16500
- onStarted: function() { started = true; }
16501
- });
16502
- Promise.resolve(sendAttempt).then(function(result) {
16503
- if (started && !(result && result.ok === false)) return;
16504
- if ((state.nextQueueRequests || []).indexOf(nextRequest) >= 0) return;
16505
- var rollbackIndex = Math.max(0, Math.min(currentIndex, state.nextQueue.length));
16506
- state.nextQueue.splice(rollbackIndex, 0, next);
16507
- state.nextQueueRequests.splice(rollbackIndex, 0, nextRequest);
16508
- }).catch(function() {
16509
- if ((state.nextQueueRequests || []).indexOf(nextRequest) >= 0) return;
16510
- var rollbackIndex = Math.max(0, Math.min(currentIndex, state.nextQueue.length));
16511
- state.nextQueue.splice(rollbackIndex, 0, next);
16512
- state.nextQueueRequests.splice(rollbackIndex, 0, nextRequest);
16513
- }).finally(function() {
16514
- if (state.nextQueueDrainsByTarget[targetKey] === drainToken) delete state.nextQueueDrainsByTarget[targetKey];
16515
- refreshNextPromptForTarget(activeTarget);
16516
- window.renderInputStack();
16517
- window.scheduleNextQueueDrain(activeTarget);
16518
- });
16519
- });
18284
+ // Backend snapshots contain the only executable queue. Rendering must never
18285
+ // remove an item or re-submit its text as a second independent command.
16520
18286
  };
16521
18287
 
16522
18288
  // === Sidebar Controls ===
@@ -17044,10 +18810,13 @@ window.refreshRightStatus = function(options) {
17044
18810
  return Promise.resolve(null);
17045
18811
  }
17046
18812
  var statusTarget = currentConversationTarget();
18813
+ var statusLoadGeneration = state.conversationLoadGeneration;
18814
+ var statusContextRequest = captureContextWindowSnapshotRequest(statusTarget);
17047
18815
  return api.getState(statusTarget).then(function(s) {
18816
+ if (!isActiveConversationTarget(statusTarget) || statusLoadGeneration !== state.conversationLoadGeneration) return null;
17048
18817
  applyAutoRouteRatingState(s);
17049
18818
  renderAutoRouteRatingControls();
17050
- if (s && s.mode) state.mode = s.mode;
18819
+ applyConversationCommandSnapshot(s, statusTarget);
17051
18820
  if (s && s.model !== undefined) state.model = s.model;
17052
18821
  if (s && s.modelLabel) state.modelLabel = s.modelLabel;
17053
18822
  if (s && s.status) state.agentStatus = s.status;
@@ -17094,28 +18863,21 @@ window.refreshRightStatus = function(options) {
17094
18863
  statusFlowRecord.lastSubmission = { name: String(s.flowRunning.name || ''), input: '', components: [] };
17095
18864
  }
17096
18865
  renderFlowTakeover(true, String(s.flowRunning.name || statusFlowRecord.name || ''), { target: statusTarget });
17097
- } else if (flowTargetMatchesCurrent(statusTarget) && s && s.flowSuspension === null && !s.flowRunning) {
18866
+ } else if (flowTargetMatchesCurrent(statusTarget) && s && s.flowSuspension === null && !s.flowRunning && !(statusFlowRecord.pendingStart && !statusFlowRecord.pendingStart.cancelled)) {
17098
18867
  // The backend authoritatively reports no Flow for this conversation.
17099
18868
  renderFlowTakeover(false, '', { target: statusTarget });
17100
18869
  }
17101
18870
  if (s && s.conversationPlan) state.conversationPlan = normalizeConversationPlan(s.conversationPlan);
17102
- if (s && s.draft !== undefined) {
17103
- var restoredDraftKey = conversationDraftKey();
17104
- if (!Object.prototype.hasOwnProperty.call(state.conversationDrafts, restoredDraftKey)) {
17105
- state.conversationDrafts[restoredDraftKey] = String(s.draft || '');
17106
- }
17107
- }
17108
- if (s && s.contextCompression !== undefined) {
17109
- state.contextCompression = s.contextCompression;
17110
- }
17111
- if (s && s.contextWindow !== undefined) {
17112
- state.contextWindow = s.contextWindow;
17113
- window.renderContextWindow();
17114
- }
18871
+ hydrateConversationDraftSnapshot(s, statusTarget);
18872
+ applyContextWindowSnapshot(s, statusContextRequest);
17115
18873
  window.renderSubagentList();
17116
18874
  if (state.rightTab === 'plan') window.renderConversationPlan();
17117
18875
  window.renderRightStatusPanel();
17118
18876
  }).catch(function(err) {
18877
+ if (!isActiveConversationTarget(statusTarget) || statusLoadGeneration !== state.conversationLoadGeneration) {
18878
+ if (options.required === true) throw err;
18879
+ return null;
18880
+ }
17119
18881
  var target = document.getElementById('right-status-content');
17120
18882
  if (target) target.innerHTML = '<div class="right-empty">' + esc(t('status.unavailable')) + ': ' + esc(err.message || String(err)) + '</div>';
17121
18883
  if (options.required === true) throw err;
@@ -18104,15 +19866,17 @@ window.closeSubWin = function() {
18104
19866
  });
18105
19867
  return;
18106
19868
  }
18107
- els['sub-win-overlay'].classList.remove('open');
18108
- state.activeSubWindowView = null;
18109
19869
  var win = els['sub-win'];
18110
- if (win) { win.classList.remove('memory-lab-window', 'settings-window'); win.style.left = ''; win.style.top = ''; win.style.margin = ''; }
18111
- var restoreFocus = subWindowOriginFocus;
18112
- subWindowOriginFocus = null;
18113
- updateApplicationInertState();
18114
- requestAnimationFrame(function() {
18115
- if (restoreFocus && restoreFocus.isConnected && typeof restoreFocus.focus === 'function') restoreFocus.focus({ preventScroll: true });
19870
+ window.animateLiquidPopupExit(win, null, function() {
19871
+ els['sub-win-overlay'].classList.remove('open');
19872
+ state.activeSubWindowView = null;
19873
+ if (win) { win.classList.remove('memory-lab-window', 'settings-window'); win.style.left = ''; win.style.top = ''; win.style.margin = ''; }
19874
+ var restoreFocus = subWindowOriginFocus;
19875
+ subWindowOriginFocus = null;
19876
+ updateApplicationInertState();
19877
+ requestAnimationFrame(function() {
19878
+ if (restoreFocus && restoreFocus.isConnected && typeof restoreFocus.focus === 'function') restoreFocus.focus({ preventScroll: true });
19879
+ });
18116
19880
  });
18117
19881
  };
18118
19882
 
@@ -18201,7 +19965,7 @@ function renderGeneralSettings() {
18201
19965
  }
18202
19966
  var backgroundColor = /^#[0-9a-f]{6}$/i.test(String(state.backgroundColor || ''))
18203
19967
  ? state.backgroundColor
18204
- : (state.theme === 'light' ? '#F0F2F8' : '#0A0A1A');
19968
+ : (state.theme === 'light' ? '#F0F2F8' : '#101010');
18205
19969
  var fontFamily = String(state.fontFamily || '');
18206
19970
  var wslOptions = (state.wslDistros || []).map(function(name) { return '<option value="' + escAttr(name) + '"' + ((state.agentBackend && state.agentBackend.distro) === name ? ' selected' : '') + '>' + esc(name) + '</option>'; }).join('');
18207
19971
  var wslHtml = state.platform === 'win32' ? '<div class="setting-row">' +
@@ -20219,13 +21983,15 @@ window.resumeInterruptedFlow = async function() {
20219
21983
  };
20220
21984
 
20221
21985
  window.resumeFlowWith = async function(feedback, isInterruptedResume) {
21986
+ var resumeTarget = currentConversationTarget();
20222
21987
  var resumeRecord = currentFlowTakeoverRecord();
20223
21988
  resumeRecord.lastRelayedError = null;
20224
21989
  try {
20225
- var result = await api.resumeFlow(feedback, currentConversationTarget());
20226
- if (result && Array.isArray(result.chatMessages)) {
20227
- if (Array.isArray(result.workRuns)) renderConversationWorkRuns(result.workRuns, currentConversationTarget());
20228
- renderChatMessages(result.chatMessages);
21990
+ var result = await api.resumeFlow(feedback, resumeTarget);
21991
+ applyConversationCommandSnapshot(result, resumeTarget);
21992
+ if (result && Array.isArray(result.chatMessages) && isActiveConversationTarget(resumeTarget)) {
21993
+ if (Array.isArray(result.workRuns)) renderConversationWorkRuns(result.workRuns, resumeTarget);
21994
+ renderChatMessages(result.chatMessages, resumeTarget);
20229
21995
  }
20230
21996
  if (result && result.alreadyRunning) {
20231
21997
  // The backend reports the Flow is already running (a stale resume click
@@ -20234,83 +22000,60 @@ window.resumeFlowWith = async function(feedback, isInterruptedResume) {
20234
22000
  // sync.
20235
22001
  resumeRecord.running = true;
20236
22002
  resumeRecord.paused = false;
20237
- window.renderFlowTakeover(true, result.flow || resumeRecord.name || (resumeRecord.lastSubmission && resumeRecord.lastSubmission.name) || '', { target: currentConversationTarget() });
22003
+ window.renderFlowTakeover(true, result.flow || resumeRecord.name || (resumeRecord.lastSubmission && resumeRecord.lastSubmission.name) || '', { target: resumeTarget });
20238
22004
  return result;
20239
22005
  }
20240
22006
  if (result && result.pending) {
20241
22007
  state.pendingOptions = result.options || [];
20242
22008
  renderPendingOptionsInChat(state.pendingOptions);
20243
22009
  if (result.interrupted) {
20244
- renderFlowTakeover(true, result.name || (resumeRecord.lastSubmission && resumeRecord.lastSubmission.name), { interrupted: true, message: result.error, target: currentConversationTarget() });
22010
+ renderFlowTakeover(true, result.name || (resumeRecord.lastSubmission && resumeRecord.lastSubmission.name), { interrupted: true, message: result.error, target: resumeTarget });
20245
22011
  }
20246
22012
  return result;
20247
22013
  }
20248
22014
  var completed = !!(result && result.ok);
20249
- var resumeTarget = currentConversationTarget();
20250
22015
  if (!completed && !flowRelayedErrorWasShown(resumeTarget)) showUiNotice(formatChatError((result && result.error) || 'Flow resume failed'), 'error', 'flow-resume-error');
20251
22016
  else if (completed) addMsg('assistant', '[System] Flow complete.', 'flow', state.model);
20252
- var settlement = stopFlowRunInternal(completed ? {} : { keepPaused: true }, resumeTarget);
20253
- await setVisibleMode('build');
20254
- if (completed && settlement && settlement.shouldDrain) window.scheduleNextQueueDrain(settlement.target);
22017
+ stopFlowRunInternal({}, resumeTarget);
22018
+ if (api.getState) applyConversationCommandSnapshot(await api.getState(resumeTarget), resumeTarget);
20255
22019
  return result;
20256
22020
  } catch (error) {
20257
- var catchTarget = currentConversationTarget();
22021
+ var catchTarget = resumeTarget;
20258
22022
  if (!flowRelayedErrorWasShown(catchTarget)) showUiNotice(formatChatError(error && error.message ? error.message : String(error), 'Flow resume failed.'), 'error', 'flow-resume-error');
20259
22023
  stopFlowRunInternal({ keepPaused: true }, catchTarget);
20260
- await setVisibleMode('build');
22024
+ if (api.getState) applyConversationCommandSnapshot(await api.getState(catchTarget), catchTarget);
20261
22025
  return { ok: false, error: error && error.message ? error.message : String(error) };
20262
22026
  }
20263
22027
  };
20264
22028
 
20265
- function removeFlowOwnedQueueRequest(lease) {
20266
- if (!lease || !lease.request) return;
20267
- var index = (state.nextQueueRequests || []).indexOf(lease.request);
20268
- if (index < 0) return;
20269
- state.nextQueue.splice(index, 1);
20270
- state.nextQueueRequests.splice(index, 1);
20271
- refreshNextPromptForTarget(lease.target);
22029
+ function removeFlowOwnedQueueRequest() {
22030
+ // Flow queue leases are owned and settled by the shared backend.
20272
22031
  }
20273
22032
 
20274
22033
  function settleFlowQueueTakeover(options, target) {
20275
- options = options || {};
20276
- target = target || currentConversationTarget();
20277
- var record = flowTakeoverRecordFor(target);
20278
- var lease = record.queueLease || null;
22034
+ var record = flowTakeoverRecordFor(target || currentConversationTarget());
20279
22035
  record.queueLease = null;
20280
- if (!lease) return null;
20281
- removeFlowOwnedQueueRequest(lease);
20282
- var key = queueRuntimeKey(lease.target);
20283
- if (!state.queuePausedByTarget) state.queuePausedByTarget = {};
20284
- state.queuePausedByTarget[key] = options.resumeQueue === true
20285
- ? false
20286
- : options.keepPaused === true
20287
- ? true
20288
- : !!lease.wasPaused;
20289
- return {
20290
- target: lease.target,
20291
- shouldDrain: !state.queuePausedByTarget[key]
20292
- };
22036
+ return null;
20293
22037
  }
20294
22038
 
20295
- window.queueFlowNextInput = function(text) {
22039
+ window.queueFlowNextInput = async function(text) {
20296
22040
  text = String(text || '').trim();
20297
22041
  if (!text) return false;
20298
22042
  var target = currentConversationTarget();
20299
- var request = bindQueuedRequestToTarget({ text: text, images: [] }, text, target, queueBranchPathForTarget(target, 'runtime'));
20300
- request.requestedMode = 'build';
20301
- request.flowQueuedDuringTakeover = true;
20302
- state.nextQueue.push(text);
20303
- state.nextQueueRequests.push(request);
20304
- pauseQueueForTarget(target);
22043
+ var result = await api.sendMessage(text, target, {
22044
+ requestedMode: 'build', inputMode: 'next', clientMessageId: window.crypto.randomUUID()
22045
+ });
22046
+ applyConversationCommandSnapshot(result, target);
20305
22047
  state.queueCollapsed = false;
20306
- refreshNextPromptForTarget(target);
20307
22048
  window.renderInputStack();
20308
- return true;
22049
+ if (result && result.ok === false) showUiNotice(result.error || 'Failed to queue Flow input.', 'error', 'flow-next');
22050
+ return result;
20309
22051
  };
20310
22052
 
20311
22053
  function stopFlowRunInternal(options, target) {
20312
22054
  target = target || currentConversationTarget();
20313
22055
  var record = flowTakeoverRecordFor(target);
22056
+ if (record.pendingStart) record.pendingStart.cancelled = true;
20314
22057
  record.running = false;
20315
22058
  record.paused = false;
20316
22059
  record.promptText = '';
@@ -20341,9 +22084,14 @@ function stopFlowRunInternal(options, target) {
20341
22084
  window.stopFlowRun = async function(options) {
20342
22085
  options = options || {};
20343
22086
  if (!flowTakeoverMatchesCurrent()) return { ok: false, action: 'not_running', error: 'The active Flow belongs to another conversation.' };
22087
+ var stopTarget = currentConversationTarget();
22088
+ var stopRecord = flowTakeoverRecordFor(stopTarget);
22089
+ if (stopRecord.pendingStart) stopRecord.pendingStart.cancelled = true;
22090
+ if (options.resumeQueue === true) return window.queueAction('queue_set_pause', { paused: false }, stopTarget);
20344
22091
  if (options.resumeQueue !== true) options.keepPaused = true;
20345
22092
  if (api.stopFlow) {
20346
- var stopResult = await api.stopFlow(currentConversationTarget()).catch(function() { return null; });
22093
+ var stopResult = await api.stopFlow(stopTarget).catch(function() { return null; });
22094
+ applyConversationCommandSnapshot(stopResult, stopTarget);
20347
22095
  var action = String(stopResult && stopResult.action || '');
20348
22096
  // First Stop/Esc while a Flow is RUNNING: cooperative abort. The flow:run
20349
22097
  // handler turns this into an interrupted suspension (renders the paused
@@ -20351,9 +22099,8 @@ window.stopFlowRun = async function(options) {
20351
22099
  if (action === 'stopping') return stopResult;
20352
22100
  }
20353
22101
  // Paused flow (or already force-stopped): tear down immediately.
20354
- var settlement = stopFlowRunInternal(options);
20355
- await setVisibleMode('build');
20356
- if (settlement && settlement.shouldDrain) window.scheduleNextQueueDrain(settlement.target);
22102
+ stopFlowRunInternal({}, stopTarget);
22103
+ if (api.getState) applyConversationCommandSnapshot(await api.getState(stopTarget), stopTarget);
20357
22104
  addMsg('assistant', '[System] Flow stopped manually.', 'flow', '');
20358
22105
  };
20359
22106
 
@@ -20412,7 +22159,8 @@ window.saveFlowToDisk = async function(workIdx) {
20412
22159
  window.closeSubWin();
20413
22160
  };
20414
22161
 
20415
- window.runFlowWork = async function(workIdx) {
22162
+ window.runFlowWork = async function(workIdx, command) {
22163
+ command = command || {};
20416
22164
  var work = state.flowWorks[workIdx];
20417
22165
  if (!work || !work.components || work.components.length === 0) {
20418
22166
  addMsg('assistant', '[System] ' + t('flow.emptyRun'), 'error', '');
@@ -20420,19 +22168,17 @@ window.runFlowWork = async function(workIdx) {
20420
22168
  return;
20421
22169
  }
20422
22170
  var normalized = window.normalizeFlowWork({ name: work.name, components: work.components || [] });
20423
- var flowInput = (els.prompt && els.prompt.value.trim()) || '';
20424
- var flowTarget = currentConversationTarget();
20425
- var flowWasPaused = window.isQueuePausedForTarget(flowTarget);
20426
- var flowQueueCount = queueIndexesForTarget(flowTarget).length;
20427
- var flowDeliveryMode = state.inputMode === 'next' ? 'next' : 'guide';
20428
- if (flowDeliveryMode === 'guide' && isCurrentConversationRunning()) {
20429
- showUiNotice(currentLang() === 'zh' ? '正在运行 Build 时,Flow 必须以 Next 提交。' : 'While a Build is running, Flow must be submitted as Next.', 'error', 'flow-guide-running');
20430
- return;
20431
- }
20432
- pauseQueueForTarget(flowTarget);
22171
+ var flowInput = typeof command.text === 'string' ? command.text : ((els.prompt && els.prompt.value.trim()) || '');
22172
+ var flowTarget = normalizeQueuedConversationTarget(command.target || currentConversationTarget());
22173
+ var flowDeliveryMode = (command.inputMode || state.inputMode) === 'next' ? 'next' : 'guide';
20433
22174
  state.queueCollapsed = false;
20434
22175
  refreshNextPromptForTarget(flowTarget);
20435
22176
  var flowRecord = flowTakeoverRecordFor(flowTarget);
22177
+ if (flowRecord.pendingStart) flowRecord.pendingStart.cancelled = true;
22178
+ // Saving and the shared command are asynchronous. Backend idle snapshots
22179
+ // cannot cancel an unsent command; only explicit stop/supersession can.
22180
+ var pendingStart = { name: normalized.name, cancelled: false };
22181
+ flowRecord.pendingStart = pendingStart;
20436
22182
  flowRecord.promptText = flowInput;
20437
22183
  flowRecord.lastSubmission = {
20438
22184
  name: normalized.name,
@@ -20443,11 +22189,6 @@ window.runFlowWork = async function(workIdx) {
20443
22189
  });
20444
22190
  })
20445
22191
  };
20446
- flowRecord.queueLease = {
20447
- target: flowTarget,
20448
- wasPaused: flowWasPaused,
20449
- request: null
20450
- };
20451
22192
  if (els.prompt) {
20452
22193
  els.prompt.value = '';
20453
22194
  els.prompt.placeholder = t('flow.runningPlaceholder');
@@ -20475,24 +22216,14 @@ window.runFlowWork = async function(workIdx) {
20475
22216
  if (api.saveFlow) {
20476
22217
  await api.saveFlow(normalized);
20477
22218
  }
20478
- while (flowRecord.running && flowWaitingForPriorBuild && isCurrentConversationRunning()) {
20479
- await new Promise(function(resolve) { setTimeout(resolve, 100); });
20480
- }
20481
- if (!flowRecord.running) return;
20482
- if (!flowRecord.runtimeLease) {
20483
- var resumedFlowRunId = 'flow-' + Date.now() + '-' + Math.random().toString(16).slice(2);
20484
- flowRecord.runtimeLease = { target: flowTarget, runId: resumedFlowRunId };
20485
- setConversationRuntimeState(flowTarget, 'running', resumedFlowRunId, { provisional: true, flow: true });
20486
- updateSubmitButtonState();
20487
- renderConversations();
20488
- }
20489
- if (api.setMode) {
20490
- await api.setMode('flow');
20491
- state._syncedMode = 'flow';
20492
- }
20493
- state.mode = 'flow';
20494
- var r = await api.runFlow(normalized.name, flowInput, 0);
20495
- if (r && r.chatMessages) {
22219
+ if (flowRecord.pendingStart !== pendingStart || pendingStart.cancelled) return;
22220
+ var r = await api.sendMessage(flowInput, flowTarget, {
22221
+ requestedMode: 'flow', inputMode: flowDeliveryMode, flowName: normalized.name,
22222
+ flowStart: 0, clientMessageId: window.crypto.randomUUID()
22223
+ });
22224
+ if (flowRecord.pendingStart !== pendingStart) return r;
22225
+ applyConversationCommandSnapshot(r, flowTarget);
22226
+ if (r && r.chatMessages && isActiveConversationTarget(flowTarget)) {
20496
22227
  if (Array.isArray(r.workRuns)) renderConversationWorkRuns(r.workRuns, flowTarget);
20497
22228
  renderChatMessages(r.chatMessages);
20498
22229
  if (r.conversations) {
@@ -20524,10 +22255,13 @@ window.runFlowWork = async function(workIdx) {
20524
22255
  showUiNotice(formatChatError(err && err.message ? err.message : String(err), 'Flow failed.'), 'error', 'flow-run-error');
20525
22256
  }
20526
22257
  } finally {
20527
- if (flowSuspended) return;
20528
- var settlement = stopFlowRunInternal(flowCompletedNormally ? {} : { keepPaused: true }, flowTarget);
20529
- await setVisibleMode('build');
20530
- if (flowCompletedNormally && settlement && settlement.shouldDrain) window.scheduleNextQueueDrain(settlement.target);
22258
+ if (flowRecord.pendingStart === pendingStart) {
22259
+ flowRecord.pendingStart = null;
22260
+ if (!flowSuspended) {
22261
+ stopFlowRunInternal({}, flowTarget);
22262
+ if (api.getState) applyConversationCommandSnapshot(await api.getState(flowTarget), flowTarget);
22263
+ }
22264
+ }
20531
22265
  }
20532
22266
  };
20533
22267
 
@@ -20612,9 +22346,10 @@ window.loadFileTree = async function(options) {
20612
22346
  }
20613
22347
  };
20614
22348
 
20615
- window.submitSelectedFlow = function() {
22349
+ window.submitSelectedFlow = function(command) {
22350
+ command = command || { text: String(els.prompt && els.prompt.value || ''), target: currentConversationTarget(), inputMode: state.inputMode };
20616
22351
  if (!state._flowsLoaded && window.ensureFlowsLoaded) {
20617
- return window.ensureFlowsLoaded().then(function() { return window.submitSelectedFlow(); });
22352
+ return window.ensureFlowsLoaded().then(function() { return window.submitSelectedFlow(command); });
20618
22353
  }
20619
22354
  var selected = String(state.defaultFlow || (document.getElementById('flow-select') && document.getElementById('flow-select').value) || '');
20620
22355
  var workIdx = state.flowWorks.findIndex(function(work) { return String(work && work.name || '') === selected; });
@@ -20625,7 +22360,7 @@ window.submitSelectedFlow = function() {
20625
22360
  showUiNotice(message, 'error', 'flow-select-required');
20626
22361
  return { ok: false, error: message };
20627
22362
  }
20628
- return window.runFlowWork(workIdx);
22363
+ return window.runFlowWork(workIdx, command);
20629
22364
  };
20630
22365
 
20631
22366
  function renderTreeNodes(nodes, parent, depth) {
@@ -21437,9 +23172,18 @@ function updateSubagentHistoryMeta(record) {
21437
23172
 
21438
23173
  window.closeSubagentHistory = function() {
21439
23174
  var overlay = document.getElementById('subagent-history-overlay');
21440
- if (overlay) overlay.remove();
21441
- state.subagentHistoryActorId = '';
21442
- _subagentLive = null;
23175
+ if (!overlay) {
23176
+ state.subagentHistoryActorId = '';
23177
+ _subagentLive = null;
23178
+ return;
23179
+ }
23180
+ var popup = overlay.querySelector('.liquid-glass-popup');
23181
+ function finish() {
23182
+ overlay.remove();
23183
+ state.subagentHistoryActorId = '';
23184
+ _subagentLive = null;
23185
+ }
23186
+ if (popup) window.animateLiquidPopupExit(popup, null, finish); else finish();
21443
23187
  };
21444
23188
 
21445
23189
  window.openSubagentHistory = function(id) {
@@ -21451,7 +23195,7 @@ window.openSubagentHistory = function(id) {
21451
23195
  }
21452
23196
  state.subagentHistoryActorId = agent.id;
21453
23197
  var existing = document.getElementById('subagent-history-overlay');
21454
- if (existing) existing.remove();
23198
+ if (existing) window.closeSubagentHistory();
21455
23199
  var lines = [];
21456
23200
  lines.push('<div style="font-size:13px;color:var(--text);font-weight:600;margin-bottom:4px;">' + esc(agent.name || agent.displayName || t('subagent.historyTitle')) + '</div>');
21457
23201
  lines.push('<div style="font-size:10px;color:var(--text-dim);word-break:break-all;margin-bottom:8px;">' + esc(agent.qualifiedName || agent.id || '') + '</div>');
@@ -21468,7 +23212,7 @@ window.openSubagentHistory = function(id) {
21468
23212
  var overlay = document.createElement('div');
21469
23213
  overlay.id = 'subagent-history-overlay';
21470
23214
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;padding:24px;';
21471
- overlay.innerHTML = '<div style="width:min(680px, calc(100vw - 48px));max-height:80vh;display:flex;flex-direction:column;background:var(--bg);border:1px solid var(--glass-border-2);border-radius:12px;padding:16px;box-shadow:0 20px 60px rgba(0,0,0,.35);">' +
23215
+ overlay.innerHTML = '<div class="liquid-glass liquid-glass-carrier liquid-glass-popup" style="width:min(680px, calc(100vw - 48px));max-height:80vh;display:flex;flex-direction:column;background:var(--bg);border:1px solid var(--glass-border-2);border-radius:12px;padding:16px;box-shadow:0 20px 60px rgba(0,0,0,.35);">' +
21472
23216
  '<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;flex:0 0 auto;"><div style="flex:1;font-size:12px;color:var(--text-dim);">' + esc(t('subagent.liveHistory')) + '</div><button type="button" style="border:1px solid var(--glass-border-1);border-radius:8px;background:var(--glass-bg-1);color:var(--text);padding:6px 10px;cursor:pointer;">' + esc(t('common.close')) + '</button></div>' +
21473
23217
  '<div style="flex:0 0 auto;">' + lines.join('') + '</div>' +
21474
23218
  '<div id="subagent-history-body" style="flex:1 1 auto;overflow:auto;margin-top:8px;min-height:80px;"></div>' +
@@ -22048,14 +23792,16 @@ function syncBackendConversation() {
22048
23792
  var conv = convs[state.activeConversation] || convs[0];
22049
23793
  var requestedConversationId = String((conv && conv.id) || '');
22050
23794
  var requestedWorkspaceKey = currentWorkspaceKey();
23795
+ var requestedLoadGeneration = state.conversationLoadGeneration;
22051
23796
  function requestIsCurrent() {
22052
- return requestedWorkspaceKey === currentWorkspaceKey() &&
23797
+ return requestedLoadGeneration === state.conversationLoadGeneration && requestedWorkspaceKey === currentWorkspaceKey() &&
22053
23798
  requestedConversationId === String(activeConversationId() || 'default');
22054
23799
  }
22055
23800
  if (conv && api.ensureConversation) {
22056
23801
  var requestedTarget = currentConversationTarget(conv.id);
22057
23802
  state.activeBackendConversationId = conv.id;
22058
23803
  var historyPromise = loadActiveConversationMessages(conv.id);
23804
+ requestedLoadGeneration = state.conversationLoadGeneration;
22059
23805
  var activate = api.activateConversation ? api.activateConversation(requestedTarget) : api.ensureConversation(requestedTarget);
22060
23806
  var activationPromise = activate.then(function(s) {
22061
23807
  var id = (s && s.conversationId) || conv.id;
@@ -22178,7 +23924,7 @@ function followConversationDragPointer() {
22178
23924
  // Keep the reorder anchor fixed at the edge, but give a held glass float a
22179
23925
  // damped visual pull so blocked dragging still feels elastic and alive.
22180
23926
  var blockedDelta = projectedTop < minTop ? projectedTop - minTop : projectedTop > maxTop ? projectedTop - maxTop : 0;
22181
- var resisted = Math.sign(blockedDelta) * Math.sqrt(Math.abs(blockedDelta)) * 0.6;
23927
+ var resisted = Math.sign(blockedDelta) * Math.min(LIQUID_POPUP_MAX_EDGE_JITTER_PX, Math.sqrt(Math.abs(blockedDelta)) * 0.6);
22182
23928
  var edge = liquidInteractionEdgePx();
22183
23929
  drag.float.style.left = (rowRect.left - edge) + 'px';
22184
23930
  drag.float.style.top = (trackTop - edge) + 'px';
@@ -22399,13 +24145,7 @@ function renderConversations() {
22399
24145
  displaySummary += ' · ' + String(conv.id || '').slice(-8);
22400
24146
  }
22401
24147
  var running = !!(runtimeState && ['running', 'stopping', 'force_restarting'].indexOf(String(runtimeState.status || '')) >= 0);
22402
- var runtimeBadge = runtimeState && runtimeState.status && runtimeState.status !== 'idle'
22403
- ? '<span class="conv-runtime-badge ' + escAttr(String(runtimeState.status)) + '">' + esc(String(runtimeState.status)) + '</span>' : '';
22404
- var branchCommIcon = conv.branchCommunication
22405
- ? '<span class="conv-branch-comm-icon" title="' + escAttr(t('conversation.branchCommunicationBadge')) + '" aria-label="' + escAttr(t('conversation.branchCommunicationBadge')) + '">' + iconSvg('git-branch', t('conversation.branchCommunicationBadge')) + '</span>' : '';
22406
- var innerKey = [String(displaySummary), String(conv.messageCount || ''), String(runtimeState && runtimeState.status || ''), conv.branchCommunication ? '1' : '0'].join('|');
22407
- var nextInner = '<span class="conv-summary" title="' + escAttr(String(conv.id || '')) + '">' + esc(displaySummary) + (conv.messageCount ? ' (' + esc(String(conv.messageCount)) + ')' : '') + '</span>' + branchCommIcon + runtimeBadge +
22408
- '<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>';
24148
+ var innerKey = [currentLang(), String(displaySummary), String(conv.messageCount || ''), String(runtimeState && runtimeState.status || ''), conv.branchCommunication ? '1' : '0'].join('|');
22409
24149
  var div = existingRows[String(conv.id || '')];
22410
24150
  if (div) {
22411
24151
  keepRows[String(conv.id || '')] = true;
@@ -22485,11 +24225,19 @@ function renderConversations() {
22485
24225
  });
22486
24226
  existingRows[String(conv.id || '')] = div;
22487
24227
  }
22488
- div.className = 'conv-item' + (conv.active ? ' active' : '');
22489
- if (running) div.classList.add('marquee-border');
22490
- div.setAttribute('tabindex', conv.active ? '0' : '-1');
22491
- div.setAttribute('aria-current', conv.active ? 'true' : 'false');
24228
+ var nextClassName = 'conv-item' + (conv.active ? ' active' : '') + (running ? ' marquee-border' : '');
24229
+ if (div.className !== nextClassName) div.className = nextClassName;
24230
+ var nextTabIndex = conv.active ? '0' : '-1';
24231
+ if (div.getAttribute('tabindex') !== nextTabIndex) div.setAttribute('tabindex', nextTabIndex);
24232
+ var nextCurrent = conv.active ? 'true' : 'false';
24233
+ if (div.getAttribute('aria-current') !== nextCurrent) div.setAttribute('aria-current', nextCurrent);
22492
24234
  if (div.getAttribute('data-inner-key') !== innerKey) {
24235
+ var runtimeBadge = runtimeState && runtimeState.status && runtimeState.status !== 'idle'
24236
+ ? '<span class="conv-runtime-badge ' + escAttr(String(runtimeState.status)) + '">' + esc(String(runtimeState.status)) + '</span>' : '';
24237
+ var branchCommIcon = conv.branchCommunication
24238
+ ? '<span class="conv-branch-comm-icon" title="' + escAttr(t('conversation.branchCommunicationBadge')) + '" aria-label="' + escAttr(t('conversation.branchCommunicationBadge')) + '">' + iconSvg('git-branch', t('conversation.branchCommunicationBadge')) + '</span>' : '';
24239
+ var nextInner = '<span class="conv-summary" title="' + escAttr(String(conv.id || '')) + '">' + esc(displaySummary) + (conv.messageCount ? ' (' + esc(String(conv.messageCount)) + ')' : '') + '</span>' + branchCommIcon + runtimeBadge +
24240
+ '<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>';
22493
24241
  div.setAttribute('data-inner-key', innerKey);
22494
24242
  div.innerHTML = nextInner;
22495
24243
  }
@@ -25702,11 +27450,14 @@ window.closeCommandSurface = function(options) {
25702
27450
  options = options || {};
25703
27451
  var overlay = document.getElementById('command-surface-overlay');
25704
27452
  if (!overlay || !overlay.classList.contains('open')) return;
25705
- overlay.classList.remove('open');
25706
- updateApplicationInertState();
27453
+ var panel = document.getElementById('command-surface');
25707
27454
  var restore = guiCommandSurfaceOriginFocus;
25708
- guiCommandSurfaceOriginFocus = null;
25709
- if (options.restoreFocus !== false) requestAnimationFrame(function(){ if (restore && restore.isConnected && typeof restore.focus === 'function') restore.focus({ preventScroll:true }); });
27455
+ window.animateLiquidPopupExit(panel, null, function() {
27456
+ overlay.classList.remove('open');
27457
+ updateApplicationInertState();
27458
+ guiCommandSurfaceOriginFocus = null;
27459
+ if (options.restoreFocus !== false) requestAnimationFrame(function(){ if (restore && restore.isConnected && typeof restore.focus === 'function') restore.focus({ preventScroll:true }); });
27460
+ });
25710
27461
  return restore;
25711
27462
  };
25712
27463
 
@@ -26197,6 +27948,7 @@ function schedulePostStartupUiRendering() {
26197
27948
  conversationId: String(s.conversationId || state.activeBackendConversationId || 'default')
26198
27949
  };
26199
27950
  if (s.runtimeKey) registerRuntimeKey(initialSnapshotTarget, s.runtimeKey);
27951
+ hydrateConversationDraftSnapshot(s, initialSnapshotTarget);
26200
27952
  if (Array.isArray(s.workRuns)) syncWorkRunsSnapshot(s.workRuns, initialSnapshotTarget);
26201
27953
  if (s.chatMessages) {
26202
27954
  hydrateConversationBranchState(s);
@@ -26318,7 +28070,8 @@ function schedulePostStartupUiRendering() {
26318
28070
  }
26319
28071
  });
26320
28072
  els.prompt.addEventListener('input', function() {
26321
- saveActiveConversationDraft();
28073
+ saveActiveConversationDraft(true);
28074
+ persistConversationDraftForKey(state._promptBoundKey || conversationDraftKey(), 700);
26322
28075
  schedulePromptInputRefresh();
26323
28076
  });
26324
28077
  els.prompt.addEventListener('paste', function(e) {
@@ -26357,6 +28110,12 @@ function schedulePostStartupUiRendering() {
26357
28110
  if (els['mode-select']) {
26358
28111
  els['mode-select'].addEventListener('change', async function() {
26359
28112
  state.mode = this.value;
28113
+ var selectedMode = state.mode;
28114
+ var selectedTarget = currentConversationTarget();
28115
+ // Submit before asynchronous Flow-list loading so an older selector
28116
+ // event cannot overwrite a newer user selection after loading finishes.
28117
+ var modeSelection = selectedMode !== state._syncedMode
28118
+ ? Promise.resolve(api.setMode(selectedMode, selectedTarget)) : Promise.resolve(selectedMode);
26360
28119
  if (window.renderInputStack) window.renderInputStack();
26361
28120
  // Flow mode: show flow selector
26362
28121
  var flowSel = document.getElementById('flow-select');
@@ -26369,33 +28128,30 @@ function schedulePostStartupUiRendering() {
26369
28128
  window.renderFlowSelector();
26370
28129
  }
26371
28130
  }
26372
- // Goal becomes active only when a Goal instruction is actually sent.
26373
- if (state.mode !== 'goal' && state.mode !== state._syncedMode) {
26374
- var modeResult = await api.setMode(state.mode);
26375
- if (modeResult != null) state._syncedMode = state.mode;
26376
- }
28131
+ // Shared selection persists the visible mode without creating a Goal
28132
+ // or replacing the mode of an already running Flow component.
28133
+ var modeResult = await modeSelection;
28134
+ if (modeResult != null && isActiveConversationTarget(selectedTarget) && state.mode === selectedMode) state._syncedMode = selectedMode;
26377
28135
  });
26378
28136
  }
26379
28137
 
26380
28138
  if (els['model-select']) {
26381
28139
  els['model-select'].addEventListener('change', async function() {
26382
- state.model = this.value;
28140
+ var selectedContextTarget = currentConversationTarget();
28141
+ var selectedContextGeneration = state.conversationLoadGeneration;
28142
+ var selectedContextModel = this.value;
28143
+ state.model = selectedContextModel;
26383
28144
  window.syncModelSelectSurface();
26384
28145
  if (state.model !== state._syncedModel) {
26385
- var modelResult = await api.setModel(state.model);
26386
- if (modelResult != null) state._syncedModel = state.model;
28146
+ var modelResult = await api.setModel(selectedContextModel);
28147
+ if (modelResult != null && isActiveConversationTarget(selectedContextTarget)
28148
+ && selectedContextGeneration === state.conversationLoadGeneration && state.model === selectedContextModel) state._syncedModel = selectedContextModel;
26387
28149
  }
26388
28150
  // The context ring/inspector must immediately reflect the newly selected
26389
28151
  // model's configured context window rather than waiting for the next send.
26390
- if (api.getState) {
26391
- var ctxTarget = currentConversationTarget();
26392
- api.getState(ctxTarget).then(function(s) {
26393
- if (s && s.contextWindow !== undefined && isActiveConversationTarget(ctxTarget) && isViewingRuntimeConversationBranch(ctxTarget)) {
26394
- state.contextWindow = s.contextWindow;
26395
- window.renderContextWindow();
26396
- }
26397
- }).catch(function(){});
26398
- }
28152
+ if (api.getState && isActiveConversationTarget(selectedContextTarget)
28153
+ && selectedContextGeneration === state.conversationLoadGeneration && state.model === selectedContextModel
28154
+ && isViewingRuntimeConversationBranch(selectedContextTarget)) scheduleActiveContextWindowRefresh(selectedContextTarget);
26399
28155
  });
26400
28156
  }
26401
28157
 
@@ -26492,3 +28248,5 @@ function schedulePostStartupUiRendering() {
26492
28248
  </script>
26493
28249
  </body>
26494
28250
  </html>
28251
+
28252
+