newmark-agent 0.6.4 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/config.example.json +13 -3
  2. package/dist/cli-commands.d.ts +1 -1
  3. package/dist/cli-commands.js +53 -2
  4. package/dist/cli-discovery.js +3 -0
  5. package/dist/conversation-utility-host.bundle.cjs +2880 -1069
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +126 -12
  8. package/dist/core/agent.js +783 -172
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +156 -20
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/config.js +27 -11
  14. package/dist/core/continuation/contracts.d.ts +1 -1
  15. package/dist/core/continuation/store.d.ts +91 -1
  16. package/dist/core/continuation/store.js +291 -61
  17. package/dist/core/conversationKernel.d.ts +49 -7
  18. package/dist/core/conversationKernel.js +303 -51
  19. package/dist/core/conversationStateDocument.d.ts +18 -0
  20. package/dist/core/conversationStateDocument.js +88 -0
  21. package/dist/core/conversationVisualBudget.d.ts +74 -0
  22. package/dist/core/conversationVisualBudget.js +153 -0
  23. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  24. package/dist/core/electronUtilityAgentClient.js +32 -8
  25. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  26. package/dist/core/electronUtilityRuntimePool.js +93 -24
  27. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  28. package/dist/core/hostRuntimeHooks.js +17 -0
  29. package/dist/core/jevDecision.d.ts +144 -0
  30. package/dist/core/jevDecision.js +226 -0
  31. package/dist/core/memoryProbe.d.ts +24 -0
  32. package/dist/core/memoryProbe.js +104 -0
  33. package/dist/core/mobilePairing.d.ts +7 -1
  34. package/dist/core/mobilePairing.js +9 -1
  35. package/dist/core/performanceDiagnostics.d.ts +1 -1
  36. package/dist/core/routeDecisionValidator.d.ts +86 -0
  37. package/dist/core/routeDecisionValidator.js +249 -0
  38. package/dist/core/routeEligibility.d.ts +52 -0
  39. package/dist/core/routeEligibility.js +108 -0
  40. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  41. package/dist/core/runtimeMemoryBudget.js +12 -0
  42. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  43. package/dist/core/utilityHostToolRouter.js +2 -0
  44. package/dist/core/visualDownscale.d.ts +6 -0
  45. package/dist/core/visualDownscale.js +137 -0
  46. package/dist/core/wslAgentClient.d.ts +1 -2
  47. package/dist/core/wslAgentClient.js +0 -8
  48. package/dist/core/wslAgentProtocol.d.ts +1 -10
  49. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  50. package/dist/core/wslAgentRuntimePool.js +17 -22
  51. package/dist/llm/provider.d.ts +3 -1
  52. package/dist/llm/provider.js +53 -9
  53. package/dist/main.js +107 -30
  54. package/dist/preload.js +3 -1
  55. package/dist/server.js +30 -18
  56. package/dist/tools/computerUse.d.ts +4 -0
  57. package/dist/tools/computerUse.js +202 -4
  58. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  59. package/dist/tools/computerUsePowerShellHost.js +105 -31
  60. package/dist/tools/index.js +4 -1
  61. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  62. package/dist/tui/src/i18n.js +12 -1
  63. package/dist/tui/src/render.js +9 -2
  64. package/dist/tui/src/settings-schema.js +19 -0
  65. package/dist/tui/src/state.js +69 -1
  66. package/dist/ui/index.html +651 -233
  67. package/dist/ui/lucide-sprite.svg +0 -8
  68. package/dist/wsl-agent-host.bundle.cjs +2685 -1044
  69. package/dist/wsl-agent-host.js +0 -3
  70. package/package.json +36 -9
@@ -185,7 +185,7 @@ try {
185
185
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
186
186
  --duration-fast: 150ms;
187
187
  --duration-normal: 250ms;
188
- --duration-slow: 400ms;
188
+ --duration-slow: 300ms;
189
189
 
190
190
  /* Stable Newmark semantic layers. Components depend on meaning, not on an
191
191
  upstream developer-preview framework's private tokens or brand palette. */
@@ -208,6 +208,17 @@ try {
208
208
  --right-width: 380px;
209
209
  --bottom-height: 200px;
210
210
  --topbar-height: 38px;
211
+
212
+ /* Component aliases. These names are referenced by existing component rules
213
+ but were never defined, which silently dropped fills, borders and danger
214
+ colors. Map them onto the theme tokens so both themes resolve them. */
215
+ --control-bg: var(--glass-bg-1);
216
+ --panel: var(--glass-bg-1);
217
+ --panel-2: var(--modal-surface);
218
+ --bg-panel: rgb(var(--glass-rgb-2));
219
+ --text-primary: var(--text);
220
+ --accent-border: color-mix(in srgb, var(--accent) 30%, transparent);
221
+ --danger: var(--nm-state-danger);
211
222
  }
212
223
 
213
224
  /* Light theme */
@@ -371,7 +382,7 @@ html, body {
371
382
  50% { transform: scale(1.04); filter: saturate(1.25); }
372
383
  }
373
384
 
374
- ::selection { background: rgba(91,120,255,0.35); color: var(--text-bright); }
385
+ ::selection { background: var(--editor-selection); color: var(--text-bright); }
375
386
 
376
387
  /* Open-source icon system: lucide-static sprite is copied into dist/ui at build time. */
377
388
  .nm-icon {
@@ -413,7 +424,10 @@ html, body {
413
424
  white-space: nowrap;
414
425
  border: 0;
415
426
  }
416
- button:focus, select:focus, input:focus, textarea:focus, [contenteditable]:focus { outline: none; }
427
+ /* Pointer focus stays quiet. The button reset goes through :where() so it keeps
428
+ (0,1,0) specificity and the keyboard :focus-visible ring below can win. */
429
+ :where(button):focus { outline: none; }
430
+ select:focus, input:focus, textarea:focus, [contenteditable]:focus { outline: none; }
417
431
  /* Liquid-glass controls use one continuous capsule edge. Equal-width icon
418
432
  buttons naturally become circles; wider controls become end-rounded pills. */
419
433
  button { border-radius: var(--radius-full) !important; }
@@ -435,6 +449,7 @@ button { border-radius: var(--radius-full) !important; }
435
449
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.10); border-radius: var(--radius-full); }
436
450
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); }
437
451
  [data-theme="light"] ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); }
452
+ [data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.2); }
438
453
 
439
454
  /* ============================================================
440
455
  LAYER 1 - App Shell
@@ -444,7 +459,7 @@ button { border-radius: var(--radius-full) !important; }
444
459
  flex-direction: column;
445
460
  height: 100vh;
446
461
  background:
447
- radial-gradient(ellipse 80% 60% at 50% 0%, rgba(91,120,255,0.06) 0%, transparent 60%),
462
+ radial-gradient(ellipse 80% 60% at 50% 0%, color-mix(in srgb, var(--accent) 6%, transparent) 0%, transparent 60%),
448
463
  radial-gradient(ellipse 60% 50% at 80% 80%, rgba(56,212,160,0.04) 0%, transparent 50%),
449
464
  var(--app-bg);
450
465
  }
@@ -516,11 +531,11 @@ button { border-radius: var(--radius-full) !important; }
516
531
  display: flex;
517
532
  align-items: center;
518
533
  justify-content: center;
519
- transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo);
534
+ transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo), transform 90ms var(--ease-out-expo);
520
535
  }
521
536
 
522
537
  .top-btn:hover { background: var(--control-hover-bg); color: var(--text-bright); }
523
- .top-btn:active { transform: scale(0.92) translateZ(0); }
538
+ .top-btn:active { transform: scale(0.92); }
524
539
  .top-btn.close-btn:hover { background: rgba(255,68,68,0.2); color: var(--nm-state-danger); }
525
540
 
526
541
  /* ============================================================
@@ -598,7 +613,7 @@ button { border-radius: var(--radius-full) !important; }
598
613
  font-size: 0;
599
614
  cursor: pointer;
600
615
  color: var(--text-dim);
601
- transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo), transform var(--duration-fast) var(--ease-out-back);
616
+ transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo), transform var(--duration-fast) var(--ease-out-expo);
602
617
  flex-shrink: 0;
603
618
  }
604
619
 
@@ -729,7 +744,7 @@ button.left-nav-icon { width: calc(100% - 12px); }
729
744
  }
730
745
 
731
746
  .left-nav-icon:hover { background: var(--control-hover-bg); color: var(--text-bright); }
732
- .left-nav-icon:active { transform: scale(0.96) translateZ(0); }
747
+ .left-nav-icon:active { transform: scale(0.96); }
733
748
  .left-nav-icon .icon { width: 24px; height: 20px; text-align: center; flex-shrink: 0; font-size: 0; }
734
749
  .left-nav-icon span:not(.icon) {
735
750
  display: inline-block;
@@ -939,7 +954,9 @@ button.left-ws-item {
939
954
  color: var(--text-dim);
940
955
  font-size: 11px;
941
956
  cursor: pointer;
942
- transition: all var(--duration-fast) var(--ease-out-expo);
957
+ transition-property: color, background-color, border-color, filter, opacity, visibility, max-height, margin-top, margin-bottom, padding-top, padding-bottom;
958
+ transition-duration: var(--duration-fast);
959
+ transition-timing-function: var(--ease-out-expo);
943
960
  flex-shrink: 0;
944
961
  max-height: 30px;
945
962
  opacity: 1;
@@ -949,7 +966,7 @@ button.left-ws-item {
949
966
  #left-ws-add:hover {
950
967
  border-color: var(--accent);
951
968
  color: var(--accent);
952
- background: rgba(91,120,255,0.06);
969
+ background: color-mix(in srgb, var(--accent) 6%, transparent);
953
970
  }
954
971
 
955
972
  #left.collapsed #left-ws-add {
@@ -990,7 +1007,7 @@ button.left-ws-item {
990
1007
  }
991
1008
 
992
1009
  .left-collapse-btn:hover { background: var(--control-hover-bg); color: var(--text-bright); }
993
- .left-collapse-btn:active { transform: scale(0.92) translateZ(0); }
1010
+ .left-collapse-btn:active { transform: scale(0.92); }
994
1011
 
995
1012
  /* ============================================================
996
1013
  LEFT SECONDARY SIDEBAR
@@ -1041,10 +1058,11 @@ button.left-ws-item {
1041
1058
  }
1042
1059
 
1043
1060
  .sec-btn:hover { background: var(--control-hover-bg); border-color: var(--border-hover); }
1044
- .sec-btn:active { transform: scale(0.96) translateZ(0); }
1061
+ .sec-btn:active { transform: scale(0.96); }
1045
1062
  .sec-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
1046
1063
  .sec-btn.primary:hover { filter: brightness(1.1); }
1047
1064
  .sec-btn:focus-visible {
1065
+ outline: none;
1048
1066
  border-color: var(--accent);
1049
1067
  box-shadow: 0 0 0 3px var(--accent-glow);
1050
1068
  }
@@ -1075,7 +1093,7 @@ button.left-ws-item {
1075
1093
  }
1076
1094
 
1077
1095
  .conv-item:hover { background: var(--glass-bg-2); border-color: var(--border-hover); }
1078
- .conv-item.active { border-color: var(--accent); background: rgba(91,120,255,0.1); }
1096
+ .conv-item.active { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
1079
1097
  .conv-item.marquee-border::before {
1080
1098
  border-radius: var(--radius-full);
1081
1099
  }
@@ -1378,13 +1396,13 @@ button.left-ws-item {
1378
1396
  display: flex;
1379
1397
  align-items: center;
1380
1398
  justify-content: center;
1381
- transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo), border-color var(--duration-fast) var(--ease-out-expo), transform var(--duration-fast) var(--ease-out-back);
1399
+ transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo), border-color var(--duration-fast) var(--ease-out-expo), transform var(--duration-fast) var(--ease-out-expo);
1382
1400
  position: relative;
1383
1401
  }
1384
1402
 
1385
1403
  .tab-btn:hover { background: var(--control-hover-bg); color: var(--text-bright); }
1386
- .tab-btn.active { background: rgba(91,120,255,0.12); color: var(--accent); border-color: rgba(91,120,255,0.2); }
1387
- .tab-btn:active { transform: scale(0.92) translateZ(0); }
1404
+ .tab-btn.active { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 20%, transparent); }
1405
+ .tab-btn:active { transform: scale(0.92); }
1388
1406
  .tab-btn .label { display: none; }
1389
1407
 
1390
1408
  .tab-divider {
@@ -1438,8 +1456,8 @@ button.left-ws-item {
1438
1456
  white-space: nowrap;
1439
1457
  }
1440
1458
 
1441
- .ft-item:hover { background: rgba(255,255,255,0.05); color: var(--text); }
1442
- .ft-item.selected { background: rgba(91,120,255,0.12); color: var(--text-bright); }
1459
+ .ft-item:hover { background: var(--control-hover-bg); color: var(--text); }
1460
+ .ft-item.selected { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--text-bright); }
1443
1461
  .ft-item .ft-icon { width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; text-align: center; flex-shrink: 0; font-size: 0; }
1444
1462
  .ft-item .ft-name { overflow: hidden; text-overflow: ellipsis; }
1445
1463
  .ft-item .ft-toggle { width: 14px; height: 16px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 0; color: var(--text-dim); transform: rotate(90deg); transition: transform var(--duration-fast) var(--ease-out-expo); }
@@ -1479,7 +1497,7 @@ button.left-ws-item {
1479
1497
  }
1480
1498
 
1481
1499
  .et-btn:hover { background: var(--control-hover-bg); color: var(--text-bright); }
1482
- .et-btn.active { background:rgba(91,120,255,.16); color:var(--accent2); border-color:rgba(91,120,255,.55); }
1500
+ .et-btn.active { background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--accent2); border-color:color-mix(in srgb, var(--accent) 55%, transparent); }
1483
1501
  .et-btn .nm-icon { width:15px; height:15px; }
1484
1502
  .et-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
1485
1503
  .et-btn.primary:hover { filter: brightness(1.1); }
@@ -1712,7 +1730,6 @@ button.left-ws-item {
1712
1730
  line-height: 0;
1713
1731
  }
1714
1732
  .math-symbol { display: inline-block; }
1715
- }
1716
1733
 
1717
1734
  /* Subagent panel */
1718
1735
  .subagent-item {
@@ -2117,7 +2134,7 @@ button.left-ws-item {
2117
2134
  }
2118
2135
 
2119
2136
  .shell-btn:hover { background: var(--control-hover-bg); color: var(--text-bright); }
2120
- .shell-btn:active { transform: scale(0.95) translateZ(0); }
2137
+ .shell-btn:active { transform: scale(0.95); }
2121
2138
 
2122
2139
  .shell-toggle-btn {
2123
2140
  margin-left: auto;
@@ -2220,7 +2237,6 @@ button.left-ws-item {
2220
2237
  .conversation-load-earlier:hover,
2221
2238
  .conversation-load-earlier:focus-visible { background: var(--glass-bg-2); border-color: var(--border-hover); outline: none; }
2222
2239
  .conversation-load-earlier:disabled { opacity: 0.6; cursor: default; }
2223
- }
2224
2240
 
2225
2241
  #chat-area:has(> .chat-msg) {
2226
2242
  background-image:
@@ -2331,11 +2347,18 @@ button.left-ws-item {
2331
2347
  line-height: 1.6;
2332
2348
  word-wrap: break-word;
2333
2349
  overflow-wrap: break-word;
2334
- transform: translateZ(0);
2335
2350
  animation: msg-in 250ms var(--ease-out-expo);
2336
2351
  position: relative;
2337
2352
  }
2338
2353
 
2354
+ /* Restored transcript rows are already present in the conversation. Avoid
2355
+ animating a large history batch or pinning every row to its own compositor
2356
+ layer; only newly arriving live messages use the entrance motion. */
2357
+ .chat-msg.history-static {
2358
+ animation: none;
2359
+ transform: none;
2360
+ }
2361
+
2339
2362
  .chat-msg::after {
2340
2363
  content: "";
2341
2364
  position: absolute;
@@ -2350,8 +2373,8 @@ button.left-ws-item {
2350
2373
  }
2351
2374
 
2352
2375
  @keyframes msg-in {
2353
- from { opacity: 0; transform: translateY(8px) translateZ(0); }
2354
- to { opacity: 1; transform: translateY(0) translateZ(0); }
2376
+ from { opacity: 0; transform: translateY(8px); }
2377
+ to { opacity: 1; transform: translateY(0); }
2355
2378
  }
2356
2379
 
2357
2380
  .chat-msg.user {
@@ -2467,7 +2490,6 @@ button.left-ws-item {
2467
2490
  }
2468
2491
 
2469
2492
  .msg-action-btn:hover { color: var(--text); background: var(--glass-bg-2); }
2470
- .msg-action-btn.route-rating-selected { color: var(--accent); background: var(--glass-bg-2); }
2471
2493
  .msg-action-btn:disabled { cursor: default; opacity: 0.62; }
2472
2494
 
2473
2495
  .message-inline-editor { display: grid; gap: 8px; margin-top: 4px; }
@@ -2487,7 +2509,7 @@ button.left-ws-item {
2487
2509
  .conversation-branch-pager { display:flex; justify-content:flex-end; align-items:center; gap:0; margin-top:7px; color:var(--text-dim); font-size:11px; }
2488
2510
  .branch-page-arrow { cursor:pointer; color:var(--text-dim); padding:1px 3px; transition:color var(--duration-fast) var(--ease-out-expo), background var(--duration-fast) var(--ease-out-expo); border-radius:var(--radius-sm); }
2489
2511
  .branch-page-arrow:hover { color:var(--text-bright); background:var(--control-hover-bg); }
2490
- .branch-page-arrow:active { transform:scale(0.92) translateZ(0); }
2512
+ .branch-page-arrow:active { transform:scale(0.92); }
2491
2513
 
2492
2514
  .chat-msg .msg-body .md-rendered {
2493
2515
  padding: 0;
@@ -2919,8 +2941,8 @@ button.todo-item { background: transparent; }
2919
2941
  margin: 8px 0;
2920
2942
  padding: 10px 14px;
2921
2943
  border-radius: var(--radius-md);
2922
- background: rgba(91,120,255,0.08);
2923
- border: 1px solid rgba(91,120,255,0.2);
2944
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
2945
+ border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
2924
2946
  }
2925
2947
 
2926
2948
  .option-question {
@@ -2945,9 +2967,9 @@ button.todo-item { background: transparent; }
2945
2967
  transition: background var(--duration-fast) var(--ease-out-expo), border-color var(--duration-fast) var(--ease-out-expo);
2946
2968
  }
2947
2969
 
2948
- .option-btn:hover { background: rgba(91,120,255,0.15); border-color: var(--accent); color: var(--text-bright); }
2949
- .option-btn:active { transform: scale(0.96) translateZ(0); }
2950
- .option-btn.selected { background:rgba(91,120,255,.24); border-color:var(--accent); color:var(--text-bright); }
2970
+ .option-btn:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); border-color: var(--accent); color: var(--text-bright); }
2971
+ .option-btn:active { transform: scale(0.96); }
2972
+ .option-btn.selected { background:color-mix(in srgb, var(--accent) 24%, transparent); border-color:var(--accent); color:var(--text-bright); }
2951
2973
  .option-btn:disabled { cursor:default; opacity:.55; }
2952
2974
  .option-btn.selected:disabled { opacity:1; }
2953
2975
  .option-desc {
@@ -2986,14 +3008,14 @@ button.todo-item { background: transparent; }
2986
3008
  padding: 0 12px;
2987
3009
  border: 1px solid var(--accent);
2988
3010
  border-radius: var(--radius-md);
2989
- background: rgba(91,120,255,0.14);
3011
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
2990
3012
  color: var(--text-bright);
2991
3013
  font: 12px var(--font-ui);
2992
3014
  cursor: pointer;
2993
3015
  transition: background var(--duration-fast) var(--ease-out-expo);
2994
3016
  }
2995
3017
 
2996
- .option-manual-submit:hover { background: rgba(91,120,255,0.24); }
3018
+ .option-manual-submit:hover { background: color-mix(in srgb, var(--accent) 24%, transparent); }
2997
3019
  .option-manual-submit:disabled { opacity: .55; cursor: default; }
2998
3020
 
2999
3021
  /* ============================================================
@@ -3043,7 +3065,7 @@ button.todo-item { background: transparent; }
3043
3065
  border-radius: 8px;
3044
3066
  box-shadow: 0 6px 18px rgba(0,0,0,0.12);
3045
3067
  overflow: hidden;
3046
- transform: translateY(0) translateZ(0);
3068
+ transform: translateY(0);
3047
3069
  animation: fade-in-up 180ms var(--ease-out-expo);
3048
3070
  }
3049
3071
  [data-theme="light"] .stack-card {
@@ -3115,6 +3137,7 @@ button.todo-item { background: transparent; }
3115
3137
  .stack-icon-btn.warn { color: var(--nm-state-warning); }
3116
3138
  .stack-icon-btn.danger { color: var(--nm-state-danger); }
3117
3139
  .stack-icon-btn:focus-visible {
3140
+ outline: none;
3118
3141
  border-color: var(--accent);
3119
3142
  box-shadow: 0 0 0 3px var(--accent-glow);
3120
3143
  }
@@ -3127,12 +3150,12 @@ button.todo-item { background: transparent; }
3127
3150
  #goal-bar {
3128
3151
  border-bottom: none;
3129
3152
  border-radius: 8px 8px 0 0;
3130
- border-color: rgba(91,120,255,0.24);
3131
- background: linear-gradient(90deg, rgba(91,120,255,0.10), rgba(24,24,24,0.94) 34%);
3153
+ border-color: color-mix(in srgb, var(--accent) 24%, transparent);
3154
+ background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 10%, transparent), rgba(24,24,24,0.94) 34%);
3132
3155
  box-shadow: none;
3133
3156
  }
3134
3157
  [data-theme="light"] #goal-bar {
3135
- background: linear-gradient(90deg, rgba(91,120,255,0.13), rgba(255,255,255,0.82) 36%, rgba(255,255,255,0.72) 100%);
3158
+ background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 13%, transparent), rgba(255,255,255,0.82) 36%, rgba(255,255,255,0.72) 100%);
3136
3159
  border-color: rgba(74,104,223,0.2);
3137
3160
  }
3138
3161
  [data-theme="light"] #goal-bar.goal-paused {
@@ -3140,7 +3163,7 @@ button.todo-item { background: transparent; }
3140
3163
  }
3141
3164
 
3142
3165
  #goal-bar .stack-row { grid-template-columns: auto minmax(0,1fr) auto; }
3143
- #goal-bar .stack-row::before { content:""; width:6px; height:6px; border-radius:50%; background:var(--accent); box-shadow:0 0 0 3px rgba(91,120,255,.12); }
3166
+ #goal-bar .stack-row::before { content:""; width:6px; height:6px; border-radius:50%; background:var(--accent); box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); }
3144
3167
  #goal-bar.goal-paused .stack-row::before { background:#f4c95d; box-shadow:0 0 0 3px rgba(244,201,93,.12); }
3145
3168
 
3146
3169
  #goal-bar .goal-text {
@@ -3222,7 +3245,7 @@ button.todo-item { background: transparent; }
3222
3245
  transition: background var(--duration-fast) var(--ease-out-expo), opacity var(--duration-fast) var(--ease-out-expo);
3223
3246
  }
3224
3247
 
3225
- .todo-item:hover { background: rgba(255,255,255,0.05); }
3248
+ .todo-item:hover { background: var(--control-hover-bg); }
3226
3249
 
3227
3250
  .todo-check {
3228
3251
  width: 14px;
@@ -3809,6 +3832,18 @@ button.todo-item { background: transparent; }
3809
3832
  position: relative;
3810
3833
  z-index: 1;
3811
3834
  }
3835
+ /* Carried content wrappers are the destination label itself, not a bare
3836
+ stacking child. They author their own inset box, and the fallback rule above
3837
+ outranks them (.liquid-selection-float > :not(...) is two class-level
3838
+ selectors against one), so their authored position has to be restated with
3839
+ the same selector weight and a later source order. Without it a pressed
3840
+ fixed control (left rail New row, rail collapse button) and a carried
3841
+ conversation row lose inset:0 / inset:9px, become shrink-to-fit flex items
3842
+ and paint left-flush inside the glass instead of staying centered. */
3843
+ .liquid-selection-float > .liquid-fixed-control-content,
3844
+ .liquid-selection-float > .conversation-drag-content {
3845
+ position: absolute;
3846
+ }
3812
3847
  @keyframes model-menu-in { from { opacity: 0; transform: translateY(5px) scale(.985); } to { opacity: 1; transform: none; } }
3813
3848
 
3814
3849
  .newmark-select-shell {
@@ -3917,6 +3952,9 @@ button.todo-item { background: transparent; }
3917
3952
 
3918
3953
  .tool-select:hover { border-color: var(--border-hover); }
3919
3954
  .tool-select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
3955
+ /* The enhanced native <select> is still a Tab stop but is transparent; mirror its
3956
+ keyboard focus on the proxy button that immediately follows it. */
3957
+ .newmark-select-native:focus-visible + .newmark-select-button { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
3920
3958
 
3921
3959
  .context-token-wrap {
3922
3960
  position: relative;
@@ -3980,6 +4018,20 @@ button.todo-item { background: transparent; }
3980
4018
  opacity: 1;
3981
4019
  transform: scale(1.08);
3982
4020
  }
4021
+ /* The ring's donut mask clips its own outline/box-shadow, so the keyboard ring is
4022
+ painted by the wrapper: 2px wide, 2px outside the 16px ring (global indicator). */
4023
+ .context-token-wrap:has(> #context-token-ring:focus-visible)::before {
4024
+ content: "";
4025
+ position: absolute;
4026
+ left: 50%;
4027
+ top: 50%;
4028
+ width: 20px;
4029
+ height: 20px;
4030
+ margin: -10px 0 0 -10px;
4031
+ border-radius: 50%;
4032
+ box-shadow: 0 0 0 2px var(--accent);
4033
+ pointer-events: none;
4034
+ }
3983
4035
 
3984
4036
  #context-token-tooltip {
3985
4037
  display: none;
@@ -4026,9 +4078,9 @@ button.todo-item { background: transparent; }
4026
4078
  transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo);
4027
4079
  }
4028
4080
 
4029
- .mode-toggle-btn:hover { background: rgba(255,255,255,0.05); color: var(--text); }
4030
- .mode-toggle-btn.active { background: rgba(91,120,255,0.15); color: var(--accent); border-radius: var(--radius-full); }
4031
- .mode-toggle-btn:active { transform: scale(0.95) translateZ(0); }
4081
+ .mode-toggle-btn:hover { background: var(--control-hover-bg); color: var(--text); }
4082
+ .mode-toggle-btn.active { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); border-radius: var(--radius-full); }
4083
+ .mode-toggle-btn:active { transform: scale(0.95); }
4032
4084
 
4033
4085
  .workspace-type-pager {
4034
4086
  display: grid;
@@ -4059,11 +4111,11 @@ button.todo-item { background: transparent; }
4059
4111
  display: flex;
4060
4112
  align-items: center;
4061
4113
  justify-content: center;
4062
- transition: transform var(--duration-fast) var(--ease-out-back), box-shadow var(--duration-fast) var(--ease-out-expo);
4114
+ transition: transform var(--duration-fast) var(--ease-out-expo), box-shadow var(--duration-fast) var(--ease-out-expo);
4063
4115
  box-shadow: 0 2px 12px var(--accent-glow);
4064
4116
  }
4065
4117
 
4066
- #submit-btn:hover { transform: scale(1.08); box-shadow: 0 4px 20px rgba(91,120,255,0.5); }
4118
+ #submit-btn:hover { transform: scale(1.08); box-shadow: 0 4px 20px color-mix(in srgb, var(--accent) 50%, transparent); }
4067
4119
  #submit-btn:active { transform: scale(0.92); transition: transform 80ms var(--ease-out-expo); }
4068
4120
  #submit-btn::after { display: none !important; }
4069
4121
  #submit-btn:active { transform: none; scale: 1; }
@@ -4189,13 +4241,13 @@ button.todo-item { background: transparent; }
4189
4241
 
4190
4242
  .working-glow {
4191
4243
  animation: none;
4192
- box-shadow: 0 0 16px var(--accent-glow), 0 0 40px rgba(91,120,255,0.1);
4193
- border-color: rgba(91,120,255,0.4) !important;
4244
+ box-shadow: 0 0 16px var(--accent-glow), 0 0 40px color-mix(in srgb, var(--accent) 10%, transparent);
4245
+ border-color: color-mix(in srgb, var(--accent) 40%, transparent) !important;
4194
4246
  }
4195
4247
 
4196
4248
  @keyframes working-border-glow {
4197
- 0%, 100% { box-shadow: 0 0 12px var(--accent-glow), 0 0 30px rgba(91,120,255,0.08); }
4198
- 50% { box-shadow: 0 0 24px rgba(91,120,255,0.5), 0 0 60px rgba(91,120,255,0.15); }
4249
+ 0%, 100% { box-shadow: 0 0 12px var(--accent-glow), 0 0 30px color-mix(in srgb, var(--accent) 8%, transparent); }
4250
+ 50% { box-shadow: 0 0 24px color-mix(in srgb, var(--accent) 50%, transparent), 0 0 60px color-mix(in srgb, var(--accent) 15%, transparent); }
4199
4251
  }
4200
4252
 
4201
4253
  /* ============================================================
@@ -4456,8 +4508,8 @@ button.todo-item { background: transparent; }
4456
4508
  transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo), border-color var(--duration-fast) var(--ease-out-expo);
4457
4509
  }
4458
4510
 
4459
- .stab-btn:hover { background: rgba(255,255,255,0.05); color: var(--text); }
4460
- .stab-btn.active { background: rgba(91,120,255,0.12); color: var(--accent); border-color: rgba(91,120,255,0.25); }
4511
+ .stab-btn:hover { background: var(--control-hover-bg); color: var(--text); }
4512
+ .stab-btn.active { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 25%, transparent); }
4461
4513
  .stab-btn:focus-visible { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-glow); color:var(--text-bright); }
4462
4514
 
4463
4515
  .plugin-tabs { overflow-x:auto; scrollbar-width:thin; }
@@ -4773,6 +4825,10 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4773
4825
  box-shadow: 0 2px 6px rgba(0,0,0,.3);
4774
4826
  transition: width 120ms var(--ease-out-expo), height 120ms var(--ease-out-expo), transform 120ms var(--ease-out-expo), box-shadow 120ms var(--ease-out-expo);
4775
4827
  }
4828
+ .setting-control input[type="range"]:focus-visible::-webkit-slider-thumb,
4829
+ #settings-glass-opacity:focus-visible::-webkit-slider-thumb {
4830
+ box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent), 0 2px 6px rgba(0,0,0,.3);
4831
+ }
4776
4832
  #settings-glass-opacity.liquid-range-dragging::-webkit-slider-thumb {
4777
4833
  width: 26px;
4778
4834
  height: 20px;
@@ -5110,9 +5166,9 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5110
5166
  .memory-lab-node.selected {
5111
5167
  min-height: 52px;
5112
5168
  border-color: var(--accent);
5113
- background: rgba(91,120,255,0.16);
5169
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
5114
5170
  color: var(--text-bright);
5115
- box-shadow: 0 0 0 1px rgba(91,120,255,0.08);
5171
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 8%, transparent);
5116
5172
  }
5117
5173
  .memory-lab-node-label {
5118
5174
  min-width: 0;
@@ -5316,8 +5372,8 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5316
5372
  }
5317
5373
  .memory-lab-overview-node.focus {
5318
5374
  border-color: var(--accent);
5319
- background: rgba(91, 120, 255, 0.2);
5320
- box-shadow: 0 0 0 5px rgba(91, 120, 255, 0.11), 0 12px 28px rgba(0,0,0,0.24);
5375
+ background: color-mix(in srgb, var(--accent) 20%, transparent);
5376
+ box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 11%, transparent), 0 12px 28px rgba(0,0,0,0.24);
5321
5377
  }
5322
5378
  .memory-lab-overview-node.dim { opacity: 0.18; }
5323
5379
  .memory-lab-overview-stage.zoom-dots .memory-lab-overview-node {
@@ -5422,9 +5478,9 @@ html.newmark-memory-overview-viewer body {
5422
5478
  }
5423
5479
  [data-theme="light"] .memory-lab-node.selected,
5424
5480
  [data-theme="light"] .memory-lab-overview-node.focus {
5425
- background: rgba(91,120,255,0.1);
5481
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
5426
5482
  color: var(--text-bright);
5427
- box-shadow: 0 0 0 3px rgba(91,120,255,0.08);
5483
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 8%, transparent);
5428
5484
  }
5429
5485
  [data-theme="light"] .memory-lab-overview-grid {
5430
5486
  opacity: 0.34;
@@ -5442,15 +5498,15 @@ html.newmark-memory-overview-viewer body {
5442
5498
  .model-chip {
5443
5499
  display:flex; align-items:center; gap:12px; box-sizing:border-box;
5444
5500
  width:100%; min-width:0; margin:6px 0; padding:10px 12px;
5445
- border-radius:var(--radius-sm); background:rgba(91,120,255,0.1);
5446
- border:1px solid rgba(91,120,255,0.2); font-size:11px; color:var(--text-accent);
5501
+ border-radius:var(--radius-sm); background:color-mix(in srgb, var(--accent) 10%, transparent);
5502
+ border:1px solid color-mix(in srgb, var(--accent) 20%, transparent); font-size:11px; color:var(--text-accent);
5447
5503
  }
5448
5504
  .model-chip-info { flex:1; min-width:0; text-align:left; overflow-wrap:anywhere; }
5449
5505
  .model-chip-actions { display:flex; flex:0 0 auto; align-items:center; gap:6px; margin-left:auto; }
5450
5506
  .model-response-warning { color:var(--text-dim); font-size:inherit; margin-left:4px; font-weight:400; }
5451
5507
  .model-enable-switch { width:38px; height:22px; padding:2px; border-radius:999px; border:1px solid var(--glass-border-1); background:var(--glass-bg-1); cursor:pointer; flex:none; }
5452
5508
  .model-enable-thumb { display:block; width:16px; height:16px; border-radius:50%; background:rgba(255,255,255,.55); transform:translateX(0); transition:transform 180ms var(--ease-out-expo); }
5453
- .model-enable-switch[aria-checked="true"] { background:rgba(91,120,255,.35); }
5509
+ .model-enable-switch[aria-checked="true"] { background:color-mix(in srgb, var(--accent) 35%, transparent); }
5454
5510
  .model-enable-switch[aria-checked="true"] .model-enable-thumb { transform:translateX(16px); }
5455
5511
  .model-enable-switch:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
5456
5512
  .model-enable-switch:disabled { opacity:.6; }
@@ -5524,7 +5580,7 @@ html.newmark-memory-overview-viewer body {
5524
5580
  padding: 8px 20px;
5525
5581
  border: 1px solid var(--accent);
5526
5582
  border-radius: var(--radius-md);
5527
- background: rgba(91,120,255,0.1);
5583
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
5528
5584
  color: var(--accent);
5529
5585
  font-size: 12px;
5530
5586
  font-family: var(--font-ui);
@@ -5532,7 +5588,7 @@ html.newmark-memory-overview-viewer body {
5532
5588
  transition: background var(--duration-fast) var(--ease-out-expo);
5533
5589
  }
5534
5590
 
5535
- .fuzzy-inject-btn:hover { background: rgba(91,120,255,0.2); }
5591
+ .fuzzy-inject-btn:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
5536
5592
 
5537
5593
  /* Archive list */
5538
5594
  .archive-item {
@@ -5729,14 +5785,14 @@ html.newmark-memory-overview-viewer body {
5729
5785
  transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo);
5730
5786
  }
5731
5787
 
5732
- .flow-new-btn:hover { background: rgba(91,120,255,0.08); color: var(--accent); border-color: var(--accent); }
5788
+ .flow-new-btn:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); color: var(--accent); border-color: var(--accent); }
5733
5789
 
5734
5790
  /* ============================================================
5735
5791
  ANIMATIONS & UTILITY
5736
5792
  ============================================================ */
5737
5793
  @keyframes fade-in-up {
5738
- from { opacity: 0; transform: translateY(12px) translateZ(0); }
5739
- to { opacity: 1; transform: translateY(0) translateZ(0); }
5794
+ from { opacity: 0; transform: translateY(12px); }
5795
+ to { opacity: 1; transform: translateY(0); }
5740
5796
  }
5741
5797
 
5742
5798
  .hidden { display: none !important; }
@@ -5962,8 +6018,12 @@ html.newmark-memory-overview-viewer body {
5962
6018
  .liquid-glass-popup,
5963
6019
  .liquid-glass-popup.liquid-popup-exit,
5964
6020
  .liquid-menu-color-block,
5965
- .liquid-selection-float { transition-duration: 1ms !important; animation-duration: 1ms !important; }
6021
+ .liquid-selection-float { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
5966
6022
  .liquid-glass-popup::before { opacity: 0; }
6023
+ /* These two-class / id !important transitions outrank the rules above. */
6024
+ .liquid-selection-float.liquid-selection-landing,
6025
+ .conversation-selection-float.conversation-drag-following { transition-duration: 0.01ms !important; }
6026
+ #left.width-animating { transition-duration: 0.01ms !important; }
5967
6027
  }
5968
6028
  @media (max-width: 900px), (max-height: 680px) {
5969
6029
  .liquid-glass-popup { --liquid-popup-blur: calc(var(--glass-blur-3) * 3); }
@@ -6058,7 +6118,7 @@ html.newmark-memory-overview-viewer body {
6058
6118
  }
6059
6119
  .liquid-glass-popup[data-liquid-popup-feedback='outward']::after { opacity: 0; }
6060
6120
  @media (prefers-reduced-motion: reduce) {
6061
- .liquid-popup-feedback-surface { transition-duration: 1ms !important; }
6121
+ .liquid-popup-feedback-surface { transition-duration: 0.01ms !important; }
6062
6122
  }
6063
6123
  .model-select-menu-option,
6064
6124
  .newmark-select-option,
@@ -6168,10 +6228,12 @@ button::after {
6168
6228
  border-radius: inherit;
6169
6229
  opacity: 0;
6170
6230
  background: var(--liquid-refraction);
6171
- backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
6172
- -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
6173
6231
  box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 4px 14px rgba(0,0,0,0.18);
6174
- transition: opacity 90ms var(--ease-out-expo);
6232
+ /* The backdrop blur is attached only while the plate can be seen. Chromium
6233
+ still paints and composites an opacity-0 layer that has a backdrop-filter,
6234
+ so a resting blur cost one backdrop layer per visible button. The delayed
6235
+ step keeps the blur through the 90ms release fade. */
6236
+ transition: opacity 90ms var(--ease-out-expo), backdrop-filter 0s linear 90ms;
6175
6237
  }
6176
6238
  button:hover:not(:disabled) {
6177
6239
  filter: brightness(1.06);
@@ -6182,7 +6244,12 @@ button:active:not(:disabled) {
6182
6244
  background-color: transparent !important;
6183
6245
  box-shadow: none;
6184
6246
  }
6185
- button:active:not(:disabled)::after { opacity: 1; }
6247
+ button:active:not(:disabled)::after {
6248
+ opacity: 1;
6249
+ backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
6250
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
6251
+ transition: opacity 90ms var(--ease-out-expo);
6252
+ }
6186
6253
  button:disabled { opacity: 0.5; }
6187
6254
 
6188
6255
  /* Unified glass controls are rendered by a real top-layer float. Their local
@@ -6220,6 +6287,21 @@ button.conversation-work-run-head::after,
6220
6287
  button.shell-block-header::after,
6221
6288
  button.diff-block-header::after,
6222
6289
  button.work-review-head::after { display: none !important; }
6290
+ /* Without a press plate, a held utility action keeps its own hover wash
6291
+ instead of dropping back to the bare row, and it does not take the
6292
+ plate's outward lift. */
6293
+ .top-btn:active:not(:disabled),
6294
+ .conv-more-btn:active:not(:disabled),
6295
+ .provider-icon-btn:active:not(:disabled),
6296
+ .work-review-toggle:active:not(:disabled) {
6297
+ scale: 1;
6298
+ background-color: var(--control-hover-bg) !important;
6299
+ }
6300
+ .msg-action-btn:active:not(:disabled) {
6301
+ scale: 1;
6302
+ background-color: var(--glass-bg-2) !important;
6303
+ }
6304
+ .top-btn.close-btn:active:not(:disabled) { background-color: rgba(255,68,68,0.2) !important; }
6223
6305
  button.conversation-work-run-head:active:not(:disabled),
6224
6306
  button.shell-block-header:active:not(:disabled),
6225
6307
  button.diff-block-header:active:not(:disabled),
@@ -6720,14 +6802,6 @@ input[type="checkbox"].liquid-switch-dragging::before {
6720
6802
  <path d="M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1" />
6721
6803
  <path d="M9 6v12" />
6722
6804
  </symbol>
6723
- <symbol id="thumbs-down" viewBox="0 0 24 24">
6724
- <path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" />
6725
- <path d="M17 14V2" />
6726
- </symbol>
6727
- <symbol id="thumbs-up" viewBox="0 0 24 24">
6728
- <path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" />
6729
- <path d="M7 10v12" />
6730
- </symbol>
6731
6805
  <symbol id="trash-2" viewBox="0 0 24 24">
6732
6806
  <path d="M10 11v6" />
6733
6807
  <path d="M14 11v6" />
@@ -7208,12 +7282,15 @@ var state = {
7208
7282
  feedbackLevel: 'default',
7209
7283
  autoSwitch: 'off',
7210
7284
  autoSwitchScope: 'all',
7285
+ /**
7286
+ * Auto decision model ("JEV") slot as reported by the backend. `locked`
7287
+ * means Auto is off, so the decision model can neither be edited nor move a
7288
+ * model; every control in the JEV card is disabled while it is true.
7289
+ */
7290
+ jevDecisionModel: null,
7211
7291
  openAIApiMode: 'chat_stream',
7212
7292
  fallbackOnUnavailable: 'off',
7213
7293
  switchTendency: 'balanced',
7214
- autoRouteRatingAvailable: false,
7215
- autoRouteRatingRouteId: '',
7216
- autoRouteRatingScore: 0,
7217
7294
  workspaces: [],
7218
7295
  currentWorkspace: '',
7219
7296
  currentWorkspaceId: '',
@@ -7225,6 +7302,10 @@ var state = {
7225
7302
  pendingOptionAnswers: {},
7226
7303
  pendingOptionBatchKey: '',
7227
7304
  pendingOptionsByTarget: {},
7305
+ // Runtime key of the conversation whose transcript is currently in #chat-area.
7306
+ // The pending-option block is appended to that transcript, so an async
7307
+ // snapshot may only (re)paint it while it still owns the on-screen area.
7308
+ renderedChatTargetKey: '',
7228
7309
  pendingOptionAnswersByTarget: {},
7229
7310
  promptAttachments: [],
7230
7311
  nextQueue: [],
@@ -7591,7 +7672,8 @@ var NEWMARK_I18N = {
7591
7672
  'model.githubModelsImported': 'Imported {count} models.',
7592
7673
  'model.githubModelsWarning': 'Warning: {warning}',
7593
7674
  'model.autoDetect': 'Auto detect',
7594
- 'model.modelName': 'Model name',
7675
+ 'model.modelName': 'API model identifier',
7676
+ 'model.displayName': 'Display name',
7595
7677
  'model.provider': 'Provider',
7596
7678
  'model.contextSize': 'Context size',
7597
7679
  'model.vision': 'Vision',
@@ -7613,15 +7695,21 @@ var NEWMARK_I18N = {
7613
7695
  'model.openAIChat': 'Chat Completions',
7614
7696
  'model.openAIResponses': 'Responses API',
7615
7697
  'model.fallback': 'Fallback unavailable model',
7616
- 'model.preference': 'Switch preference',
7698
+ 'model.preference': 'Model decision tendency',
7699
+ 'model.preferenceDesc': 'The current eligible model makes each Auto choice. This setting guides how readily it favors a different eligible model; it does not select a model or set a numeric cutoff.',
7617
7700
  'model.costSaving': 'Cost saving',
7618
7701
  'model.performance': 'Quality',
7619
- 'model.qualityPolicy': 'Quality · 0% loss band',
7620
- 'model.balancedPolicy': 'Balanced · 2% loss band',
7621
- 'model.costPolicy': 'Cost · 6% loss band',
7622
- 'model.speedPolicy': 'Speed · 4% loss band',
7623
- 'model.learnedPreference': 'Adaptive preference',
7624
- 'model.clearLearnedPreference': 'Clear learned preference',
7702
+ 'model.conservativeTendency': 'Conservative',
7703
+ 'model.balancedTendency': 'Balanced',
7704
+ 'model.aggressiveTendency': 'Aggressive',
7705
+ 'model.jevTitle': 'Auto decision policy',
7706
+ 'model.jevDesc': 'Uses the current eligible conversation model with native thinking disabled and strict JSON Schema output. Invalid output, unsupported controls, or timeout stops Auto routing.',
7707
+ 'model.jevEnabled': 'Current-model decision enabled',
7708
+ 'model.jevCurrentModelStatus': 'Current eligible model · thinking off · strict JSON Schema',
7709
+ 'model.jevTimeout': 'Decision timeout (ms)',
7710
+ 'model.jevLocked': 'Auto is off: current-model decision settings are locked and no model may change.',
7711
+ 'model.jevSaved': 'Auto decision policy updated.',
7712
+ 'model.jevRefused': 'The Auto decision policy was not changed.',
7625
7713
  'model.validationTitle': 'Model validation',
7626
7714
  'model.validating': 'Validating model latency and capabilities...',
7627
7715
  'model.validationProgress': 'Validation progress',
@@ -8334,7 +8422,8 @@ var NEWMARK_I18N = {
8334
8422
  'model.githubModelsImported': '已导入 {count} 个模型。',
8335
8423
  'model.githubModelsWarning': '警告:{warning}',
8336
8424
  'model.autoDetect': '自动检测',
8337
- 'model.modelName': '模型名称',
8425
+ 'model.modelName': '调用模型标识',
8426
+ 'model.displayName': '显示名称',
8338
8427
  'model.provider': '供应商',
8339
8428
  'model.contextSize': '上下文规模',
8340
8429
  'model.vision': '视觉',
@@ -8356,15 +8445,21 @@ var NEWMARK_I18N = {
8356
8445
  'model.openAIChat': 'Chat Completions',
8357
8446
  'model.openAIResponses': 'Responses API',
8358
8447
  'model.fallback': '模型不可用时回退',
8359
- 'model.preference': '切换倾向',
8448
+ 'model.preference': '模型决策倾向',
8449
+ 'model.preferenceDesc': '每次 Auto 选择均由当前符合条件的模型决策。此设置只影响模型的决策倾向,不替模型选择部署,也不设数值阈值。',
8360
8450
  'model.costSaving': '节省优先',
8361
8451
  'model.performance': '质量优先',
8362
- 'model.qualityPolicy': '质量 · 0% 损失带',
8363
- 'model.balancedPolicy': '均衡 · 2% 损失带',
8364
- 'model.costPolicy': '成本 · 6% 损失带',
8365
- 'model.speedPolicy': '速度 · 4% 损失带',
8366
- 'model.learnedPreference': '自适应偏好',
8367
- 'model.clearLearnedPreference': '清空学习偏好',
8452
+ 'model.conservativeTendency': '保守',
8453
+ 'model.balancedTendency': '均衡',
8454
+ 'model.aggressiveTendency': '激进',
8455
+ 'model.jevTitle': 'Auto 决策策略',
8456
+ 'model.jevDesc': '仅使用当前符合条件的对话模型,并原生关闭思考、要求严格 JSON Schema 输出。输出无效、关闭思考不受支持或请求超时都会停止 Auto 路由。',
8457
+ 'model.jevEnabled': '启用当前模型决策',
8458
+ 'model.jevCurrentModelStatus': '当前符合条件的模型 · 已关闭思考 · 严格 JSON Schema',
8459
+ 'model.jevTimeout': '决策超时(毫秒)',
8460
+ 'model.jevLocked': 'Auto 已关闭:当前模型决策设置已锁定,任何模型都不会变动。',
8461
+ 'model.jevSaved': '已更新 Auto 决策策略。',
8462
+ 'model.jevRefused': 'Auto 决策策略未做任何修改。',
8368
8463
  'model.validationTitle': '模型校验',
8369
8464
  'model.validating': '正在校验模型延迟和能力...',
8370
8465
  'model.validationProgress': '校验进度',
@@ -9652,87 +9747,6 @@ window.copyMessageText = async function(button) {
9652
9747
  var branchApiOverride = null;
9653
9748
  window.__setBranchApiForTest = function(nextApi) { branchApiOverride = nextApi || null; };
9654
9749
 
9655
- function applyAutoRouteRatingState(snapshot) {
9656
- snapshot = snapshot || {};
9657
- var decision = snapshot.routeDecision && typeof snapshot.routeDecision === 'object' ? snapshot.routeDecision : null;
9658
- var routeId = decision ? String(decision.routeId || '') : '';
9659
- var selection = decision && decision.requestedSelection;
9660
- var available = !!routeId && !!decision.resolvedDeployment && selection && selection.kind === 'auto'
9661
- && snapshot.autoRouteRatingAvailable !== false;
9662
- if (routeId !== state.autoRouteRatingRouteId) state.autoRouteRatingScore = 0;
9663
- state.autoRouteRatingAvailable = !!available;
9664
- state.autoRouteRatingRouteId = available ? routeId : '';
9665
- if (!available) state.autoRouteRatingScore = 0;
9666
- }
9667
-
9668
- function autoRouteRatingLabel(score) {
9669
- if (currentLang() === 'zh') return score > 0 ? '此 Auto 路由有帮助' : '此 Auto 路由没有帮助';
9670
- return score > 0 ? 'This Auto route was helpful' : 'This Auto route was not helpful';
9671
- }
9672
-
9673
- function attachAutoRouteRatingControls(message) {
9674
- if (!message || !api.rateAutoRoute || !state.autoRouteRatingAvailable || !state.autoRouteRatingRouteId) return;
9675
- var actions = message.querySelector('.msg-actions');
9676
- if (!actions) return;
9677
- var old = actions.querySelectorAll('.route-rating-btn');
9678
- for (var i = 0; i < old.length; i++) old[i].remove();
9679
- [1, -1].forEach(function(score) {
9680
- var label = autoRouteRatingLabel(score);
9681
- var selected = Number(state.autoRouteRatingScore || 0) === score;
9682
- var button = document.createElement('button');
9683
- button.type = 'button';
9684
- button.className = 'msg-action-btn route-rating-btn' + (selected ? ' route-rating-selected' : '');
9685
- button.title = label;
9686
- button.setAttribute('aria-label', label);
9687
- button.setAttribute('aria-pressed', selected ? 'true' : 'false');
9688
- button.setAttribute('data-route-id', state.autoRouteRatingRouteId);
9689
- button.innerHTML = iconSvg(score > 0 ? 'thumbs-up' : 'thumbs-down', label, 'tiny');
9690
- button.disabled = !!state.autoRouteRatingScore;
9691
- button.addEventListener('click', function() { window.rateAutoRoute(button, score); });
9692
- actions.appendChild(button);
9693
- });
9694
- }
9695
-
9696
- function renderAutoRouteRatingControls() {
9697
- if (!els['chat-area']) return;
9698
- var existing = els['chat-area'].querySelectorAll('.route-rating-btn');
9699
- for (var i = 0; i < existing.length; i++) existing[i].remove();
9700
- if (!state.autoRouteRatingAvailable) return;
9701
- var messages = els['chat-area'].querySelectorAll('.chat-msg.assistant:not(.work-run-message)');
9702
- attachAutoRouteRatingControls(messages.length ? messages[messages.length - 1] : null);
9703
- }
9704
-
9705
- window.rateAutoRoute = async function(button, score) {
9706
- if (!api.rateAutoRoute || !button) return;
9707
- var message = button.closest ? button.closest('.chat-msg') : null;
9708
- var buttons = message ? message.querySelectorAll('.route-rating-btn') : [];
9709
- for (var i = 0; i < buttons.length; i++) buttons[i].disabled = true;
9710
- var routeId = String(button.getAttribute('data-route-id') || '');
9711
- try {
9712
- var result = await api.rateAutoRoute({
9713
- target: currentConversationTarget(),
9714
- score: score,
9715
- routeId: routeId
9716
- });
9717
- if (result && (result.ok || result.reason === 'already_rated')) {
9718
- state.autoRouteRatingScore = Number(result.score) === -1 || Number(result.score) === 1 ? Number(result.score) : score;
9719
- renderAutoRouteRatingControls();
9720
- showUiNotice(currentLang() === 'zh' ? '已记录 Auto 路由反馈。' : 'Auto route feedback recorded.', 'info', 'auto-route-rating-' + routeId);
9721
- return;
9722
- }
9723
- for (var j = 0; j < buttons.length; j++) buttons[j].disabled = false;
9724
- var reason = result && result.reason ? String(result.reason) : 'no_active_auto_route';
9725
- showUiNotice(
9726
- currentLang() === 'zh' ? '无法记录 Auto 路由反馈:' + reason : 'Unable to record Auto route feedback: ' + reason,
9727
- 'error',
9728
- 'auto-route-rating-' + routeId
9729
- );
9730
- } catch (error) {
9731
- for (var k = 0; k < buttons.length; k++) buttons[k].disabled = false;
9732
- showUiNotice(error && error.message ? error.message : String(error), 'error', 'auto-route-rating-' + routeId);
9733
- }
9734
- };
9735
-
9736
9750
  window.editUserMessage = function(button, messageIndex) {
9737
9751
  var message = button && button.closest ? button.closest('.chat-msg') : null;
9738
9752
  if (!message || message.querySelector('.message-inline-editor')) return;
@@ -10185,7 +10199,8 @@ function appendConversationImageAttachments(container, input) {
10185
10199
  }
10186
10200
 
10187
10201
  function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10188
- var chatWasAtBottom = !!(els['chat-area'] && shouldAutoScroll(els['chat-area']));
10202
+ var skipAutoScroll = !!(messageMeta && messageMeta.skipAutoScroll);
10203
+ var chatWasAtBottom = !skipAutoScroll && !!(els['chat-area'] && shouldAutoScroll(els['chat-area']));
10189
10204
  text = redactSensitiveText(text);
10190
10205
  var owningRunId = messageMeta && String(messageMeta.runId || '');
10191
10206
  if (owningRunId && role === 'assistant' && els['chat-area']) {
@@ -10216,6 +10231,7 @@ function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10216
10231
  }
10217
10232
  var div = document.createElement('div');
10218
10233
  div.className = 'chat-msg ' + role;
10234
+ if (messageMeta && messageMeta.skipEntryAnimation) div.classList.add('history-static');
10219
10235
  var metaLabel = role === 'user'
10220
10236
  ? (String(mode || '').toLowerCase() === 'flow-user-input' ? t('flow.userInput') : t('message.user'))
10221
10237
  : (role === 'workflow' ? t('message.workflow') : (role === 'system' ? t('message.system') : t('message.agent')));
@@ -10232,7 +10248,11 @@ function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10232
10248
  if (model) metaInfo += (metaInfo ? ' | ' : '') + t('message.model') + ': ' + model;
10233
10249
  div.innerHTML = '<div class="meta"><span class="msg-role">' + metaLabel + '</span><span> | ' + esc(displayTimestamp) + '</span>' +
10234
10250
  (metaInfo ? '<span class="meta-extra"> | ' + esc(metaInfo) + '</span>' : '') + messageActionsHtml(role, text, messageIndex) + '</div><div class="msg-body">' + renderMessageContent(text) + '</div>';
10251
+ var msgActions = role === 'user' ? div.querySelector('.msg-actions') : null;
10252
+ div._newmarkEditUserButton = msgActions && msgActions.children.length > 1 ? msgActions.children[1] : null;
10235
10253
  div._newmarkMessageText = String(text || '');
10254
+ div._newmarkMessageMode = String(mode || '');
10255
+ div._newmarkMessageModel = String(model || '');
10236
10256
  div._newmarkMessageAttachments = appendConversationImageAttachments(div.querySelector('.msg-body'), messageMeta && messageMeta.attachments);
10237
10257
  div._newmarkDisplayImages = role === 'assistant' && owningRunId
10238
10258
  ? appendWorkDisplayImages(div.querySelector('.msg-body'), owningRunId, messageMeta && messageMeta.target)
@@ -10255,7 +10275,7 @@ function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10255
10275
  else els['chat-area'].appendChild(div);
10256
10276
  } else els['chat-area'].appendChild(div);
10257
10277
  if (chatWasAtBottom) _chatShouldAutoScroll = true;
10258
- autoScrollIfAtBottom();
10278
+ if (!skipAutoScroll) autoScrollIfAtBottom();
10259
10279
  return div;
10260
10280
  }
10261
10281
 
@@ -10473,6 +10493,8 @@ function updateMsg(div, text, role, mode, model, options) {
10473
10493
  if (preserveRunFinalResponse) div.classList.add('run-final-response');
10474
10494
  text = redactSensitiveText(text);
10475
10495
  div._newmarkMessageText = String(text || '');
10496
+ if (mode !== undefined && mode !== null) div._newmarkMessageMode = String(mode || '');
10497
+ if (model !== undefined && model !== null) div._newmarkMessageModel = String(model || '');
10476
10498
  var body = div.querySelector('.msg-body');
10477
10499
  var streaming = !!(options && options.streaming);
10478
10500
  if (body) {
@@ -10961,6 +10983,7 @@ function setConversationRuntimeState(target, status, runId, extra) {
10961
10983
  if (!state.runningConversations) state.runningConversations = {};
10962
10984
  if (['running', 'stopping', 'force_restarting'].indexOf(next.status) >= 0) state.runningConversations[key] = next;
10963
10985
  else delete state.runningConversations[key];
10986
+ if (typeof ensureRuntimeReconcileWatchdog === 'function') ensureRuntimeReconcileWatchdog();
10964
10987
  var branchIds = conversationBranchIdsForTarget(target);
10965
10988
  var runs = workRunsForBranch(target, branchIds.runtime || branchIds.viewed);
10966
10989
  var matchingRun = next.runId && runs.find(function(item) { return String(item.runId || '') === next.runId; });
@@ -11980,7 +12003,10 @@ function updateConversationWorkRunElement(run, element) {
11980
12003
  // 重建/增量更新前后保持聊天视窗滚动位置:贴底时跟随最新,未贴底时
11981
12004
  // 精确恢复 scrollTop,避免内容高度变化把视窗“强制拖向最新”。
11982
12005
  var chatEl = els['chat-area'];
11983
- var wasAtBottom = !!(chatEl && shouldAutoScroll(chatEl));
12006
+ // The passive scroll listener keeps this sticky-bottom bit current. Re-read
12007
+ // scrollHeight here would force layout immediately before the DOM patch on
12008
+ // every incremental update, then the restoration below would force it again.
12009
+ var wasAtBottom = !!(chatEl && _chatShouldAutoScroll);
11984
12010
  var scrollTopBefore = chatEl ? chatEl.scrollTop : 0;
11985
12011
  var activityOpenStates = {};
11986
12012
  var priorActivities = element.querySelectorAll ? element.querySelectorAll('details.conversation-work-activity[data-activity-key], details[data-work-detail-key]') : [];
@@ -12525,7 +12551,19 @@ function cacheConversationMessages(messages, target) {
12525
12551
  var key = branchConversationViewKey(scopedTarget);
12526
12552
  var cloned = Array.isArray(messages) ? messages.map(function(message) { return Object.assign({}, message); }) : [];
12527
12553
  if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
12554
+ // Disk remains authoritative. Limit retained branch snapshots, not history.
12555
+ delete state.conversationMessagesByTarget[key];
12528
12556
  state.conversationMessagesByTarget[key] = cloned;
12557
+ var cacheKeys = Object.keys(state.conversationMessagesByTarget);
12558
+ var retainedChars = 0;
12559
+ for (var i = cacheKeys.length - 1; i >= 0; i--) {
12560
+ var cachedKey = cacheKeys[i];
12561
+ var rows = state.conversationMessagesByTarget[cachedKey];
12562
+ var chars = rows.reduce(function(sum, row) { return sum + String(row.content || '').length; }, 0);
12563
+ if (cacheKeys.length - i > 6 || retainedChars + chars > 4 * 1024 * 1024) {
12564
+ delete state.conversationMessagesByTarget[cachedKey];
12565
+ } else retainedChars += chars;
12566
+ }
12529
12567
  return cloned.map(function(message) { return Object.assign({}, message); });
12530
12568
  }
12531
12569
 
@@ -12616,6 +12654,9 @@ function renderChatMessagesUnsafe(messages, target) {
12616
12654
  var chatWasAtBottom = shouldAutoScroll(els['chat-area']);
12617
12655
  els['chat-area'].innerHTML = '';
12618
12656
  if (chatWasAtBottom) _chatShouldAutoScroll = true;
12657
+ // This target now owns the transcript on screen; an async snapshot for it may
12658
+ // repaint its held option block, and a snapshot for any other target may not.
12659
+ state.renderedChatTargetKey = runtimeKeyFor(renderTarget.workspaceId, renderTarget.conversationId);
12619
12660
  var renderConversationId = renderTarget.conversationId;
12620
12661
  var guideMessages = guideMessagesForTarget(renderTarget);
12621
12662
  var persistedGuideIds = {};
@@ -12632,6 +12673,10 @@ function renderChatMessagesUnsafe(messages, target) {
12632
12673
  });
12633
12674
  var persistedRunsById = {};
12634
12675
  var persistedRunIndexes = {};
12676
+ // dev-0.6.6: the transcript loop below asks "is this message owned by a
12677
+ // persisted work run?" once per message. A linear scan there is O(rounds²)
12678
+ // for a long conversation, so resolve it through a lookup table instead.
12679
+ var persistedRunIdSet = {};
12635
12680
  var renderedRunIds = {};
12636
12681
  var recoveredAssistantRunIds = {};
12637
12682
  var messageRunRoles = {};
@@ -12641,6 +12686,7 @@ function renderChatMessagesUnsafe(messages, target) {
12641
12686
  if (runId) {
12642
12687
  persistedRunsById[runId] = run;
12643
12688
  persistedRunIndexes[runId] = runIndex;
12689
+ persistedRunIdSet[runId] = true;
12644
12690
  }
12645
12691
  });
12646
12692
  normalizedMessages.forEach(function(message, messageIndex) {
@@ -12701,7 +12747,7 @@ function renderChatMessagesUnsafe(messages, target) {
12701
12747
  finishToolBatch(renderConversationId);
12702
12748
  var clientMessageId = String(m.clientMessageId || '');
12703
12749
  if (clientMessageId) persistedGuideIds[clientMessageId] = true;
12704
- if (clientMessageId && m.runId && persistedRuns.some(function(run) { return String(run.runId || '') === String(m.runId || ''); })) {
12750
+ if (clientMessageId && m.runId && persistedRunIdSet[String(m.runId || '')]) {
12705
12751
  delete guideMessages[clientMessageId];
12706
12752
  continue;
12707
12753
  }
@@ -12720,7 +12766,9 @@ function renderChatMessagesUnsafe(messages, target) {
12720
12766
  guideId: m.guideId || '',
12721
12767
  attachments: m.attachments || [],
12722
12768
  target: renderTarget,
12723
- timestamp: m.timestamp || ''
12769
+ timestamp: m.timestamp || '',
12770
+ skipAutoScroll: true,
12771
+ skipEntryAnimation: true
12724
12772
  });
12725
12773
  if (associatedRun && String(m.role || '') === 'user') renderOwnedWorkRun(associatedRun, false);
12726
12774
  if (clientMessageId) delete guideMessages[clientMessageId];
@@ -12733,7 +12781,6 @@ function renderChatMessagesUnsafe(messages, target) {
12733
12781
  });
12734
12782
  if (pendingWorkReview) addWorkReview(pendingWorkReview.diffs || pendingWorkReview, pendingWorkReview.runId || '');
12735
12783
  renderConversationBranchPagers();
12736
- renderAutoRouteRatingControls();
12737
12784
  if (typeof renderPendingOptionsInChat === 'function') renderPendingOptionsInChat(state.pendingOptions || []);
12738
12785
  }
12739
12786
 
@@ -12742,6 +12789,153 @@ function renderChatMessagesUnsafe(messages, target) {
12742
12789
  // the preceding window via conversation:loadEarlier and prepends it, so a
12743
12790
  // long-running conversation can be browsed without ever transferring the whole
12744
12791
  // transcript in one snapshot.
12792
+ function canPrependHistoryMessages(messages, existingMessages, target, liveArea, previousStart, pageStart) {
12793
+ if (!Array.isArray(messages) || !messages.length || !Array.isArray(existingMessages) || !existingMessages.length || !liveArea) return false;
12794
+ if (!Number.isFinite(Number(previousStart)) || !Number.isFinite(Number(pageStart))
12795
+ || Number(pageStart) < 0 || Number(pageStart) + messages.length !== Number(previousStart)) return false;
12796
+ var targetKey = runtimeKeyFor(target.workspaceId, target.conversationId);
12797
+ if (state.renderedChatTargetKey !== targetKey) return false;
12798
+ var persistedRuns = workRunsForTarget(target).slice();
12799
+ var persistedRunIds = Object.create(null);
12800
+ var stableRunStatuses = ['completed', 'interrupted', 'force_interrupted', 'error'];
12801
+ for (var runIndex = 0; runIndex < persistedRuns.length; runIndex++) {
12802
+ var persistedRun = persistedRuns[runIndex] || {};
12803
+ var persistedRunId = String(persistedRun.runId || '');
12804
+ if (!persistedRunId || stableRunStatuses.indexOf(String(persistedRun.status || '')) < 0) return false;
12805
+ persistedRunIds[persistedRunId] = true;
12806
+ }
12807
+ if ((state.conversationBranchGroups || []).length || (state.conversationBranches || []).length > 1
12808
+ || state.activeConversationBranchId || (state.viewedConversationBranchNodePath || []).length) return false;
12809
+ var workUi = state.agentWorkUiByConversation && state.agentWorkUiByConversation[targetKey];
12810
+ if (workUi && (workUi.pendingWorkReview || (workUi.workReviews || []).length)) return false;
12811
+ if (pendingOptionsForTarget(target).length) return false;
12812
+
12813
+ function isPlainHistoryMessage(message, allowPersistedRun) {
12814
+ if (!message || (message.role !== 'user' && message.role !== 'assistant')) return false;
12815
+ var messageMode = String(message.mode || '').trim().toLowerCase();
12816
+ if (messageMode && messageMode !== 'chat' && messageMode !== 'build') return false;
12817
+ if (message.clientMessageId || message.guideId || isHiddenWorkflowMessage(message)) return false;
12818
+ if (Array.isArray(message.attachments) && message.attachments.length) return false;
12819
+ var runId = String(message.runId || '');
12820
+ if (runId && persistedRunIds[runId] && !allowPersistedRun) return false;
12821
+ return true;
12822
+ }
12823
+ if (!messages.every(function(message) { return isPlainHistoryMessage(message, false); })
12824
+ || !existingMessages.every(function(message) { return isPlainHistoryMessage(message, true); })) return false;
12825
+
12826
+ var contentNodes = Array.prototype.filter.call(liveArea.children, function(node) {
12827
+ return !node.classList.contains('conversation-load-earlier');
12828
+ });
12829
+ var messageNodes = contentNodes.filter(function(node) {
12830
+ return node.classList.contains('chat-msg')
12831
+ && !node.classList.contains('work-run-message')
12832
+ && node.hasAttribute('data-message-index');
12833
+ });
12834
+ if (messageNodes.length !== existingMessages.length) return false;
12835
+ var auxiliaryNodes = contentNodes.filter(function(node) {
12836
+ return !node.classList.contains('chat-msg')
12837
+ || node.classList.contains('work-run-message')
12838
+ || !node.hasAttribute('data-message-index');
12839
+ });
12840
+ if (!auxiliaryNodes.every(function(node) {
12841
+ if (!node.classList.contains('chat-msg')) return false;
12842
+ if (node.classList.contains('work-run-message')) {
12843
+ var runElement = node.querySelector('.conversation-work-run[data-run-id]');
12844
+ return !!runElement && !!persistedRunIds[String(runElement.getAttribute('data-run-id') || '')];
12845
+ }
12846
+ return node.classList.contains('run-final-response')
12847
+ && !!persistedRunIds[String(node.getAttribute('data-run-id') || '')];
12848
+ })) return false;
12849
+ var assistantRunIds = Object.create(null);
12850
+ var orderedMessages = messages.concat(existingMessages);
12851
+ for (var i = 0; i < orderedMessages.length; i++) {
12852
+ var message = orderedMessages[i];
12853
+ var runId = String(message && message.runId || '');
12854
+ if (message && message.role === 'assistant' && runId) {
12855
+ if (assistantRunIds[runId]) return false;
12856
+ assistantRunIds[runId] = true;
12857
+ }
12858
+ }
12859
+ return messageNodes.every(function(node, index) {
12860
+ var message = existingMessages[index] || {};
12861
+ var expectedId = String(message.messageId || '');
12862
+ var nodeTarget = node._newmarkMessageTarget;
12863
+ return node.classList.contains('chat-msg')
12864
+ && node.classList.contains(String(message.role || ''))
12865
+ && Number(node.getAttribute('data-message-index')) === index
12866
+ && String(node.getAttribute('data-message-id') || '') === expectedId
12867
+ && String(node.getAttribute('data-run-id') || '') === String(message.runId || '')
12868
+ && String(node._newmarkMessageMode || '').trim().toLowerCase() === String(message.mode || '').trim().toLowerCase()
12869
+ && String(node._newmarkMessageModel || '').trim() === String(message.model || '').trim()
12870
+ && String(node._newmarkMessageText || '') === String(message.content || '')
12871
+ && !!nodeTarget && nodeTarget.workspaceId === String(target.workspaceId || '')
12872
+ && nodeTarget.conversationId === String(target.conversationId || 'default');
12873
+ });
12874
+ }
12875
+
12876
+ function prependHistoryMessagesIncrementally(messages, existingMessages, target, previousStart, pageStart) {
12877
+ var liveArea = els['chat-area'];
12878
+ if (!canPrependHistoryMessages(messages, existingMessages, target, liveArea, previousStart, pageStart)) return false;
12879
+
12880
+ var contentNodes = Array.prototype.filter.call(liveArea.children, function(node) {
12881
+ return !node.classList.contains('conversation-load-earlier');
12882
+ });
12883
+ var anchor = contentNodes[0] || null;
12884
+ var anchorTop = anchor ? anchor.getBoundingClientRect().top : 0;
12885
+ var wasAtBottom = shouldAutoScroll(liveArea);
12886
+ var previousAutoScroll = _chatShouldAutoScroll;
12887
+ var stagingArea = document.createElement('div');
12888
+ var priorArea = els['chat-area'];
12889
+ var renderFailed = false;
12890
+ try {
12891
+ els['chat-area'] = stagingArea;
12892
+ messages.forEach(function(message, index) {
12893
+ addMsg(message.role, message.content || '', message.mode || '', message.model || '', index, {
12894
+ runId: message.runId || '',
12895
+ messageId: message.messageId || '',
12896
+ target: target,
12897
+ timestamp: message.timestamp || '',
12898
+ skipAutoScroll: true,
12899
+ skipEntryAnimation: true
12900
+ });
12901
+ });
12902
+ } catch (error) {
12903
+ renderFailed = true;
12904
+ console.warn('[ui_history_prepend_failed]', error);
12905
+ } finally {
12906
+ els['chat-area'] = priorArea;
12907
+ _chatShouldAutoScroll = previousAutoScroll;
12908
+ }
12909
+ if (renderFailed || stagingArea.childNodes.length !== messages.length) return false;
12910
+
12911
+ var shiftedBy = messages.length;
12912
+ contentNodes.forEach(function(node) {
12913
+ if (!node.classList.contains('chat-msg') || !node.hasAttribute('data-message-index')) return;
12914
+ var nextIndex = Number(node.getAttribute('data-message-index')) + shiftedBy;
12915
+ node.setAttribute('data-message-index', String(nextIndex));
12916
+ var editButton = node._newmarkEditUserButton;
12917
+ if (editButton) editButton.setAttribute('onclick', 'window.editUserMessage(this,' + nextIndex + ')');
12918
+ });
12919
+ var fragment = document.createDocumentFragment();
12920
+ while (stagingArea.firstChild) fragment.appendChild(stagingArea.firstChild);
12921
+ liveArea.insertBefore(fragment, anchor);
12922
+
12923
+ var runAnchorIndexes = workRunAnchorIndexStore(target);
12924
+ Object.keys(runAnchorIndexes).forEach(function(runId) {
12925
+ var anchorIndex = Number(runAnchorIndexes[runId]);
12926
+ if (Number.isFinite(anchorIndex) && anchorIndex >= 0) runAnchorIndexes[runId] = anchorIndex + shiftedBy;
12927
+ });
12928
+
12929
+ var targetKey = runtimeKeyFor(target.workspaceId, target.conversationId);
12930
+ state.renderedChatMessages = cacheConversationMessages(messages.concat(existingMessages), target);
12931
+ state.renderedChatTargetKey = targetKey;
12932
+ _chatShouldAutoScroll = wasAtBottom;
12933
+ renderLoadEarlierButton();
12934
+ if (wasAtBottom) liveArea.scrollTop = liveArea.scrollHeight;
12935
+ else if (anchor) liveArea.scrollTop += anchor.getBoundingClientRect().top - anchorTop;
12936
+ return true;
12937
+ }
12938
+
12745
12939
  function renderLoadEarlierButton() {
12746
12940
  if (!els['chat-area']) return;
12747
12941
  Array.prototype.forEach.call(els['chat-area'].querySelectorAll('.conversation-load-earlier'), function(button) { button.remove(); });
@@ -12772,9 +12966,12 @@ function renderLoadEarlierButton() {
12772
12966
  var mergedMessages = result.chatMessages.concat(newer);
12773
12967
  state.conversationLoadedBefore[loadKey] = Math.max(0, Number(result.windowStart) || 0);
12774
12968
  var chatWasAtBottom = shouldAutoScroll(els['chat-area']);
12775
- renderChatMessages(mergedMessages, loadTarget);
12776
- if (!chatWasAtBottom && els['chat-area']) els['chat-area'].scrollTop = 0;
12777
- renderLoadEarlierButton();
12969
+ var prepended = prependHistoryMessagesIncrementally(result.chatMessages, newer, loadTarget, loadBefore, result.windowStart);
12970
+ if (!prepended) {
12971
+ renderChatMessages(mergedMessages, loadTarget);
12972
+ if (!chatWasAtBottom && els['chat-area']) els['chat-area'].scrollTop = 0;
12973
+ renderLoadEarlierButton();
12974
+ }
12778
12975
  }).catch(function() { button.disabled = false; });
12779
12976
  });
12780
12977
  els['chat-area'].insertBefore(button, els['chat-area'].firstChild);
@@ -12921,6 +13118,8 @@ function scheduleStreamingWorkflowTextFlush(ui, eventConversationId, eventWorksp
12921
13118
  requestAnimationFrame(function() {
12922
13119
  var current = conversationWorkUiState(eventConversationId, eventWorkspaceId);
12923
13120
  current._streamFlushPending = false;
13121
+ // A queued frame can outlive navigation or its UI state object.
13122
+ if (current !== ui || !isActiveConversationTarget({ workspaceId: eventWorkspaceId, conversationId: eventConversationId })) return;
12924
13123
  applyStreamingWorkflowText(current, eventConversationId, current._streamMode || state.mode, current._streamModel || state.model);
12925
13124
  });
12926
13125
  }
@@ -13392,6 +13591,15 @@ function applyConversationModelSelection(snapshot) {
13392
13591
  }
13393
13592
  }
13394
13593
 
13594
+ /**
13595
+ * dev-0.6.6 长程内存回收:渲染端启动时清掉主进程记住的修订号(页面重载后内存里
13596
+ * 的转录已不存在,必须重新取全量)。之后每次真正画完一份转录都会 ack。
13597
+ */
13598
+ (function clearTranscriptRevisionsOnBoot() {
13599
+ if (!api || !api.ackTranscript) return;
13600
+ try { api.ackTranscript(currentConversationTarget(), ''); } catch (e) {}
13601
+ })();
13602
+
13395
13603
  function applyConversationSnapshot(s, requestedConversationId, options) {
13396
13604
  if (!s) return;
13397
13605
  if (s.computerUse) {
@@ -13411,23 +13619,34 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13411
13619
  var preserveReadableHistory = !!(options && options.preserveReadableHistory
13412
13620
  && !snapshotHasReadableConversationHistory(s)
13413
13621
  && (!options.allowEmptyHistory || conversationHasReadableHistory(snapshotTarget)));
13622
+ // dev-0.6.6 长程内存回收:宿主可能只回传"转录未变"的瘦快照(仅当本渲染端此前
13623
+ // 通过 ackTranscript 声明过同一修订号)。瘦快照只对"当前正画着这条对话"有效;
13624
+ // 其它情况必须取一次全量,绝不能用别的对话的画面顶替。
13625
+ var transcriptUnchanged = !!(s && s.transcriptUnchanged === true);
13414
13626
  if (s && s.runtimeKey) registerRuntimeKey(snapshotTarget, s.runtimeKey);
13627
+ if (transcriptUnchanged && !preserveReadableHistory) {
13628
+ var unchangedKey = runtimeKeyFor(snapshotTarget.workspaceId, snapshotTarget.conversationId);
13629
+ if (String(state.renderedChatTargetKey || '') !== unchangedKey && api.getState) {
13630
+ api.getState(snapshotTarget, { transcript: 'force' }).then(function(full) {
13631
+ if (full && isActiveConversationTarget(snapshotTarget)) applyConversationSnapshot(full, snapshotTarget.conversationId, options);
13632
+ }).catch(function() {});
13633
+ }
13634
+ }
13415
13635
  hydrateConversationDraftSnapshot(s, snapshotTarget);
13416
- hydrateConversationBranchState(s);
13636
+ if (!transcriptUnchanged) hydrateConversationBranchState(s);
13417
13637
  var modelRevisionMatches = !options || options.modelRevision === undefined
13418
13638
  || Number(state.modelSelectionRevision || 0) === Number(options.modelRevision || 0);
13419
13639
  if (modelRevisionMatches && isActiveConversationTarget(snapshotTarget)) {
13420
13640
  applyConversationModelSelection(s);
13421
13641
  }
13422
13642
  rebindQueueToRuntimeBranch(snapshotTarget);
13423
- if (s && Array.isArray(s.workRuns) && !preserveReadableHistory) {
13643
+ if (s && Array.isArray(s.workRuns) && !preserveReadableHistory && !transcriptUnchanged) {
13424
13644
  var viewedBranchIdForSync = Array.isArray(s.viewedBranchNodePath) && s.viewedBranchNodePath.length
13425
13645
  ? String(s.viewedBranchNodePath[s.viewedBranchNodePath.length - 1])
13426
13646
  : '';
13427
13647
  syncWorkRunsSnapshot(s.workRuns, snapshotTarget, viewedBranchIdForSync || undefined);
13428
13648
  }
13429
- applyAutoRouteRatingState(s);
13430
- if (s && Number.isFinite(Number(s.totalMessages))) {
13649
+ if (s && Number.isFinite(Number(s.totalMessages)) && Array.isArray(s.chatMessages)) {
13431
13650
  if (!state.conversationLoadedBefore) state.conversationLoadedBefore = {};
13432
13651
  var windowKey = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
13433
13652
  state.conversationLoadedBefore[windowKey] = Math.max(0, Number(s.totalMessages) - (Array.isArray(s.chatMessages) ? s.chatMessages.length : 0));
@@ -13439,9 +13658,16 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13439
13658
  window.renderContextWindow();
13440
13659
  }
13441
13660
  if (state.rightTab === 'status') window.renderRightStatusPanel();
13442
- if (s && Array.isArray(s.chatMessages) && !preserveReadableHistory) {
13661
+ if (s && Array.isArray(s.chatMessages) && !preserveReadableHistory && !transcriptUnchanged) {
13443
13662
  renderChatMessages(s.chatMessages, snapshotTarget);
13444
13663
  renderLoadEarlierButton();
13664
+ // 声明"我已画好该修订",宿主下一次轮询才会回瘦快照。只在真正画到屏幕上
13665
+ // (或该目标就是当前渲染目标)时声明。
13666
+ if (typeof s.transcriptRevision === 'string' && s.transcriptRevision
13667
+ && String(state.renderedChatTargetKey || '') === runtimeKeyFor(snapshotTarget.workspaceId, snapshotTarget.conversationId)
13668
+ && api.ackTranscript) {
13669
+ try { api.ackTranscript(snapshotTarget, s.transcriptRevision); } catch (e) {}
13670
+ }
13445
13671
  }
13446
13672
  if (s && s.conversationId) {
13447
13673
  state.activeBackendConversationId = s.conversationId;
@@ -13453,6 +13679,17 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13453
13679
  if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, snapshotTarget);
13454
13680
  }
13455
13681
  syncQueueItemsFromSnapshot(s, snapshotTarget);
13682
+ // The transcript above is painted from state.pendingOptions, but the
13683
+ // per-target option store is only refreshed by the line just executed.
13684
+ // A conversation that asked its question while another conversation was
13685
+ // focused therefore has to be repainted here, otherwise the held question
13686
+ // never appears when the user returns to the conversation that asked it.
13687
+ if (isActiveConversationTarget(snapshotTarget)
13688
+ && state.renderedChatTargetKey === runtimeKeyFor(snapshotTarget.workspaceId, snapshotTarget.conversationId)) {
13689
+ // Paint from the same projection renderChatMessages uses, so an already
13690
+ // answered question (cleared optimistically) is never resurrected.
13691
+ renderPendingOptionsInChat(state.pendingOptions || []);
13692
+ }
13456
13693
  if (s && Array.isArray(s.workEvents)) {
13457
13694
  var eventConversationId = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
13458
13695
  if (!state.agentWorkEventsByConversation) state.agentWorkEventsByConversation = {};
@@ -13472,13 +13709,29 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13472
13709
  if (s && s.subagents) state.subagents = s.subagents;
13473
13710
  if (s && s.linkedPlan) state.linkedPlan = s.linkedPlan;
13474
13711
  replayActiveAgentWorkEvents();
13475
- // Ensure running state reflects work runs from snapshot for freshly loaded conversations
13712
+ // Ensure running state reflects work runs from snapshot for freshly loaded
13713
+ // conversations. dev-0.6.6: a persisted `running` work run is NOT evidence
13714
+ // that the conversation is still running — a crashed utility host leaves the
13715
+ // record behind forever. Only a snapshot that reports a live run may
13716
+ // resurrect the running state; otherwise close the stale record locally so
13717
+ // the timer stops and the next input is a new send instead of a queued Next
13718
+ // (the reported "timestamp keeps running but nothing works" state).
13719
+ var runtimeSnapshot = s && s.runtime ? s.runtime : null;
13720
+ var snapshotStatusText = String(s && s.status || '').toLowerCase();
13721
+ var backendStillRunning = !!(runtimeSnapshot && (runtimeSnapshot.running || runtimeSnapshot.stopRequested))
13722
+ || ['running', 'stopping', 'force_restarting'].indexOf(snapshotStatusText) >= 0;
13476
13723
  var runs = state.workRunsByTarget && state.workRunsByTarget[runtimeKeyFor(snapshotTarget.workspaceId, snapshotTarget.conversationId)] || [];
13477
13724
  for (var ri = 0; ri < runs.length; ri++) {
13478
- if (runs[ri].status === 'running') {
13725
+ if (runs[ri].status !== 'running') continue;
13726
+ if (backendStillRunning) {
13479
13727
  setConversationRuntimeState(snapshotTarget, 'running', runs[ri].runId);
13728
+ continue;
13480
13729
  }
13730
+ runs[ri].status = 'interrupted';
13731
+ if (!runs[ri].endedAt) runs[ri].endedAt = new Date().toISOString();
13732
+ if (typeof updateConversationWorkRunElement === 'function') updateConversationWorkRunElement(runs[ri]);
13481
13733
  }
13734
+ if (typeof reconcileLocalRuntimeFromSnapshot === 'function') reconcileLocalRuntimeFromSnapshot(s, snapshotTarget);
13482
13735
  var isRunning = !!runningConversationRecord(snapshotTarget.conversationId, snapshotTarget.workspaceId);
13483
13736
  setWorking(isRunning);
13484
13737
  }
@@ -13913,6 +14166,66 @@ async function reconcileStaleLocalRuntimeBeforeSend(target) {
13913
14166
  return true;
13914
14167
  }
13915
14168
 
14169
+ /**
14170
+ * dev-0.6.6: 用权威快照纠正「本地还在跑、后端其实已经死了」的状态。
14171
+ *
14172
+ * 真实现场:utility host 进程被 V8 OOM 杀死后不会再发终态事件,渲染端于是永远
14173
+ * 停在「运行中」——计时一直在走、输入被当成 Next 排进队列、队列也永远等不到执行者。
14174
+ * 这里用已经在手的快照做纠正(不额外请求 IPC),并且绝不与渲染端在飞的发送抢状态。
14175
+ */
14176
+ function reconcileLocalRuntimeFromSnapshot(snapshot, target) {
14177
+ if (!snapshot || typeof snapshot !== 'object') return false;
14178
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
14179
+ if (!owner) return false;
14180
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
14181
+ if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[key]) return false;
14182
+ if (!runningConversationRecord(owner.conversationId, owner.workspaceId)) return false;
14183
+ var runtimeSnapshot = snapshot.runtime || null;
14184
+ var snapshotStatus = String(snapshot.status || '').toLowerCase();
14185
+ var backendStillRunning = !!(runtimeSnapshot && (runtimeSnapshot.running || runtimeSnapshot.stopRequested))
14186
+ || ['running', 'stopping', 'force_restarting'].indexOf(snapshotStatus) >= 0;
14187
+ if (backendStillRunning) return false;
14188
+ if (['idle', 'completed', 'interrupted', 'force_interrupted', 'error'].indexOf(snapshotStatus) < 0) return false;
14189
+ var runId = String((runtimeSnapshot && runtimeSnapshot.runId) || snapshot.runId || '');
14190
+ var generation = Number((runtimeSnapshot && runtimeSnapshot.generation) || snapshot.generation || 0);
14191
+ setConversationRuntimeState(owner, snapshotStatus, runId, { provisional: false, generation: generation });
14192
+ if (state.nextQueueSchedulesByTarget) {
14193
+ delete state.nextQueueSchedulesByTarget[key];
14194
+ delete state.nextQueueSchedulesByTarget[runtimeKeyFor('', owner.conversationId)];
14195
+ }
14196
+ state._sendInFlight = Object.keys(state.runningConversations || {}).length > 0;
14197
+ setWorking(state._sendInFlight);
14198
+ updateSubmitButtonState();
14199
+ if (window.renderInputStack) window.renderInputStack();
14200
+ return true;
14201
+ }
14202
+
14203
+ /**
14204
+ * While the renderer believes a conversation is running, verify that belief
14205
+ * against the backend. A crashed host cannot publish its own terminal event, so
14206
+ * the watchdog is the only thing that can clear the phantom "running" state.
14207
+ */
14208
+ var _runtimeReconcileWatchdog = null;
14209
+ function ensureRuntimeReconcileWatchdog() {
14210
+ if (_runtimeReconcileWatchdog || !api.getState) return;
14211
+ _runtimeReconcileWatchdog = setInterval(function() {
14212
+ var records = Object.keys(state.runningConversations || {});
14213
+ if (!records.length) {
14214
+ clearInterval(_runtimeReconcileWatchdog);
14215
+ _runtimeReconcileWatchdog = null;
14216
+ return;
14217
+ }
14218
+ records.forEach(function(key) {
14219
+ var record = (state.runningConversations || {})[key];
14220
+ var target = record && record.target ? record.target : null;
14221
+ if (!target || !target.conversationId) return;
14222
+ Promise.resolve(api.getState(target)).then(function(snapshot) {
14223
+ reconcileLocalRuntimeFromSnapshot(snapshot, target);
14224
+ }).catch(function() {});
14225
+ });
14226
+ }, 15000);
14227
+ }
14228
+
13916
14229
  function setSubmitButtonVisual(icon, label, running, stopMode) {
13917
14230
  var btn = els['submit-btn'] || document.getElementById('submit-btn');
13918
14231
  if (!btn) return;
@@ -14596,19 +14909,14 @@ function modelDeploymentValueForName(name, providerId) {
14596
14909
  var modelName = String(name || '').trim();
14597
14910
  if (!modelName) return '';
14598
14911
  var providers = Array.isArray(state.providers) ? state.providers : [];
14599
- var match = null;
14600
- for (var pi = 0; pi < providers.length && !match; pi++) {
14601
- var provider = providers[pi];
14602
- if (providerId && String(provider.id || provider.name || '') !== String(providerId)) continue;
14603
- var models = Array.isArray(provider.models) ? provider.models : [];
14604
- for (var mi = 0; mi < models.length && !match; mi++) {
14605
- var entryName = typeof models[mi] === 'string' ? models[mi] : (models[mi] && models[mi].name || '');
14606
- if (entryName === modelName) match = { provider: provider, name: entryName };
14607
- }
14608
- }
14609
- if (!match) return modelName;
14610
- var providerIdValue = String(match.provider.id || match.provider.name || '');
14611
- return 'deployment:' + encodeURIComponent(providerIdValue) + ':' + encodeURIComponent(match.name);
14912
+ var matches = [];
14913
+ providers.forEach(function(provider) {
14914
+ if (providerId && String(provider.id || '') !== String(providerId)) return;
14915
+ if ((provider.models || []).some(function(model) { return (typeof model === 'string' ? model : model.name) === modelName; })) matches.push(provider);
14916
+ });
14917
+ // Missing/ambiguous identities must never select another provider's credentials.
14918
+ if (matches.length !== 1 || !matches[0].id) return '';
14919
+ return 'deployment:' + encodeURIComponent(matches[0].id) + ':' + encodeURIComponent(modelName);
14612
14920
  }
14613
14921
 
14614
14922
  // 不可用模型回退同步输入框下方选择区:固定模型选择被回退时,选择区
@@ -18500,8 +18808,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18500
18808
  addMsg('user', displayText, opts.fromQueue ? 'next-build' : requestedMode, state.model, opts.optimisticMessageIndex, { attachments: optimisticAttachments });
18501
18809
  }
18502
18810
  renderConversationBranchPagers();
18503
- applyAutoRouteRatingState(null);
18504
- renderAutoRouteRatingControls();
18505
18811
  }
18506
18812
  var responseMsg = null;
18507
18813
  var sendFailure = '';
@@ -18545,7 +18851,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18545
18851
  setConversationRuntimeState(lockedTarget, receivedSendStatus, r.runId, { provisional: false, generation: r.generation || 0, runtimeKey: r.runtimeKey || '' });
18546
18852
  }
18547
18853
  var stillActive = sendResultOwnsRuntime && sendLoadGeneration === state.conversationLoadGeneration && isActiveConversationTarget(lockedTarget) && isViewingRuntimeConversationBranch(lockedTarget);
18548
- if (stillActive) applyAutoRouteRatingState(r);
18549
18854
  if (stillActive && r && Array.isArray(r.workRuns)) syncWorkRunsSnapshot(r.workRuns, lockedTarget);
18550
18855
  var responseOwnedByRun = !!(r && Array.isArray(r.workRuns) && r.workRuns.some(function(run) {
18551
18856
  return (run.events || []).some(function(event) { return ['response', 'final_response'].indexOf(String(event.type || '').toLowerCase()) >= 0; });
@@ -18648,7 +18953,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18648
18953
  }
18649
18954
  if (stillActiveAfterRefresh && s && s.conversationPlan) state.conversationPlan = normalizeConversationPlan(s.conversationPlan);
18650
18955
  if (stillActiveAfterRefresh && s && Array.isArray(s.workRuns)) {
18651
- applyAutoRouteRatingState(s);
18652
18956
  hydrateConversationBranchState(s);
18653
18957
  syncWorkRunsSnapshot(s.workRuns, lockedTarget);
18654
18958
  if (Array.isArray(s.chatMessages)) renderChatMessages(s.chatMessages, lockedTarget);
@@ -19241,8 +19545,6 @@ window.refreshRightStatus = function(options) {
19241
19545
  var statusContextRequest = captureContextWindowSnapshotRequest(statusTarget);
19242
19546
  return api.getState(statusTarget).then(function(s) {
19243
19547
  if (!isActiveConversationTarget(statusTarget) || statusLoadGeneration !== state.conversationLoadGeneration) return null;
19244
- applyAutoRouteRatingState(s);
19245
- renderAutoRouteRatingControls();
19246
19548
  applyConversationCommandSnapshot(s, statusTarget);
19247
19549
  if (s && s.model !== undefined) state.model = s.model;
19248
19550
  if (s && s.modelLabel) state.modelLabel = s.modelLabel;
@@ -20516,6 +20818,29 @@ function renderGeneralSettings() {
20516
20818
  '</div>';
20517
20819
  }
20518
20820
 
20821
+ /**
20822
+ * Current-conversation-model decision policy. No separate JEV model, runtime,
20823
+ * model ID, artifact path, or selector is exposed here. When Auto is off every
20824
+ * policy control is locked.
20825
+ */
20826
+ function jevDecisionModelSettingsHtml() {
20827
+ var jev = state.jevDecisionModel && typeof state.jevDecisionModel === 'object' ? state.jevDecisionModel : {};
20828
+ var locked = jev.locked === true || state.autoSwitch === 'off';
20829
+ var disabled = locked ? ' disabled' : '';
20830
+ var enabled = jev.enabled !== false;
20831
+ var currentModelStatus = t('model.jevCurrentModelStatus');
20832
+ return '<div class="provider-card" id="jev-decision-model-card" data-jev-locked="' + (locked ? 'true' : 'false') + '" style="margin-top:12px;">' +
20833
+ '<div id="jev-decision-model-title" style="font-size:13px;font-weight:700;color:var(--text);margin-bottom:4px;">' + esc(t('model.jevTitle')) + '</div>' +
20834
+ '<div id="jev-decision-model-desc" style="font-size:11px;color:var(--text-dim);line-height:1.45;margin-bottom:6px;">' + esc(t('model.jevDesc')) + '</div>' +
20835
+ (locked ? '<div id="jev-decision-model-lock" class="model-eval-pending" style="font-size:11px;margin-bottom:6px;">' + esc(t('model.jevLocked')) + '</div>' : '') +
20836
+ '<div class="setting-row"><span class="setting-label">' + esc(t('model.jevEnabled')) + '</span><div class="setting-control">' +
20837
+ '<input type="checkbox" id="jev-field-enabled" data-jev-field="enabled" role="switch"' + (enabled ? ' checked' : '') + disabled + ' onchange="window.setJevDecisionModel({enabled:this.checked})"></div></div>' +
20838
+ '<div class="setting-row"><span class="setting-label">' + esc(t('model.jevTimeout')) + '</span><div class="setting-control">' +
20839
+ '<input type="number" id="jev-field-timeout" data-jev-field="timeout-ms" min="100" max="30000" step="100" style="width:90px;" value="' + escAttr(String(Number(jev.timeoutMs) || 3000)) + '"' + disabled + ' onchange="window.setJevDecisionModel({timeoutMs:Number(this.value)})"></div></div>' +
20840
+ '<div id="jev-decision-model-status" data-jev-model="current-eligible-model" data-jev-thinking="off" data-jev-output="strict-json-schema" style="font-size:11px;color:var(--text-dim);margin-top:4px;">' + esc(currentModelStatus) + '</div>' +
20841
+ '</div>';
20842
+ }
20843
+
20519
20844
  function renderModelSettings() {
20520
20845
  var providersHtml = '';
20521
20846
  for (var i = 0; i < state.providers.length; i++) {
@@ -20591,11 +20916,11 @@ function renderModelSettings() {
20591
20916
  '<option value="off"' + (state.fallbackOnUnavailable === 'off' ? ' selected' : '') + '>' + esc(t('common.off')) + '</option>' +
20592
20917
  '<option value="on"' + (state.fallbackOnUnavailable === 'on' ? ' selected' : '') + '>' + esc(t('common.on')) + '</option></select></div></div>' +
20593
20918
  '<div class="setting-row"><span class="setting-label">' + esc(t('model.preference')) + '</span><div class="setting-control"><select onchange="window.setSwitchTendency(this.value)">' +
20594
- '<option value="quality"' + (state.switchTendency === 'quality' ? ' selected' : '') + '>' + esc(t('model.qualityPolicy')) + '</option>' +
20595
- '<option value="balanced"' + (state.switchTendency === 'balanced' ? ' selected' : '') + '>' + esc(t('model.balancedPolicy')) + '</option>' +
20596
- '<option value="cost"' + (state.switchTendency === 'cost' ? ' selected' : '') + '>' + esc(t('model.costPolicy')) + '</option>' +
20597
- '<option value="speed"' + (state.switchTendency === 'speed' ? ' selected' : '') + '>' + esc(t('model.speedPolicy')) + '</option></select></div></div>' +
20598
- '<div class="setting-row"><span class="setting-label">' + esc(t('model.learnedPreference')) + '</span><div class="setting-control"><button type="button" class="sec-btn settings-action-btn" onclick="window.clearLearnedAutoPreferences()">' + esc(t('model.clearLearnedPreference')) + '</button></div></div>' +
20919
+ '<option value="conservative"' + (state.switchTendency === 'conservative' ? ' selected' : '') + '>' + esc(t('model.conservativeTendency')) + '</option>' +
20920
+ '<option value="balanced"' + (state.switchTendency === 'balanced' ? ' selected' : '') + '>' + esc(t('model.balancedTendency')) + '</option>' +
20921
+ '<option value="aggressive"' + (state.switchTendency === 'aggressive' ? ' selected' : '') + '>' + esc(t('model.aggressiveTendency')) + '</option></select></div></div>' +
20922
+ '<div class="setting-desc" style="margin:4px 0 12px;">' + esc(t('model.preferenceDesc')) + '</div>' +
20923
+ jevDecisionModelSettingsHtml() +
20599
20924
  '</div>' +
20600
20925
  '<div class="provider-card" style="margin-top:12px;">' +
20601
20926
  '<label for="global-agent-prompt" style="display:block;font-size:13px;font-weight:700;color:var(--text);margin-bottom:6px;">' + esc(t('model.globalAgentPrompt')) + '</label>' +
@@ -21076,6 +21401,47 @@ window.setAutoSwitchMode = function(v) {
21076
21401
  state.autoSwitchScope = v === 'provider' ? 'provider' : 'all';
21077
21402
  api.saveConfig({ autoSwitch: state.autoSwitch, autoSwitchScope: state.autoSwitchScope });
21078
21403
  window.refreshModelSelect();
21404
+ // Auto off locks the decision-model editor; Auto on unlocks it again. Ask the
21405
+ // backend for the authoritative lock state instead of guessing locally.
21406
+ window.refreshJevDecisionModel();
21407
+ };
21408
+
21409
+ window.refreshJevDecisionModel = function() {
21410
+ return api.getState().then(function(s) {
21411
+ window.applyJevDecisionModelState(s);
21412
+ var panel = document.getElementById('stab-models');
21413
+ if (panel && panel.getAttribute('data-lazy') !== '1') panel.innerHTML = renderModelSettings();
21414
+ return state.jevDecisionModel;
21415
+ }).catch(function() { return state.jevDecisionModel; });
21416
+ };
21417
+
21418
+ window.applyJevDecisionModelState = function(snapshot) {
21419
+ if (snapshot && typeof snapshot === 'object' && snapshot.jevDecisionModel && typeof snapshot.jevDecisionModel === 'object') {
21420
+ state.jevDecisionModel = snapshot.jevDecisionModel;
21421
+ }
21422
+ };
21423
+
21424
+ /**
21425
+ * Persist one decision-model field. The backend is authoritative: it refuses
21426
+ * every edit while Auto is off (`auto_disabled`) and rejects invalid values, so
21427
+ * the UI never claims a change that did not happen.
21428
+ */
21429
+ window.setJevDecisionModel = function(patch) {
21430
+ if (!api.setJevDecisionModel) return Promise.resolve(null);
21431
+ return api.setJevDecisionModel(patch || {}).then(function(result) {
21432
+ if (result && result.state) state.jevDecisionModel = result.state;
21433
+ var panel = document.getElementById('stab-models');
21434
+ if (panel) panel.innerHTML = renderModelSettings();
21435
+ if (!result || result.ok === false) {
21436
+ showUiNotice(result && result.reason === 'auto_disabled' ? t('model.jevLocked') : t('model.jevRefused'), 'error', 'jev-decision-model');
21437
+ } else {
21438
+ showUiNotice(t('model.jevSaved'), 'info', 'jev-decision-model');
21439
+ }
21440
+ return result;
21441
+ }).catch(function(error) {
21442
+ showUiNotice(t('model.jevRefused') + ' ' + (error && error.message ? error.message : String(error)), 'error', 'jev-decision-model');
21443
+ return null;
21444
+ });
21079
21445
  };
21080
21446
 
21081
21447
  var systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)');
@@ -21218,14 +21584,10 @@ window.setFallbackOnUnavailable = function(v) {
21218
21584
  };
21219
21585
 
21220
21586
  window.setSwitchTendency = function(v) {
21221
- state.switchTendency = ['quality', 'balanced', 'cost', 'speed'].indexOf(v) >= 0 ? v : 'balanced';
21587
+ state.switchTendency = ['conservative', 'balanced', 'aggressive'].indexOf(v) >= 0 ? v : 'balanced';
21222
21588
  api.saveConfig({ switchTendency: state.switchTendency });
21223
21589
  };
21224
21590
 
21225
- window.clearLearnedAutoPreferences = function() {
21226
- api.saveConfig({ clearLearnedAutoPreferences: true }).catch(function(){});
21227
- };
21228
-
21229
21591
  window.setNativeToolEnabled = function(name, enabled) {
21230
21592
  if (!name) return;
21231
21593
  state.nativeToolEnabled = state.nativeToolEnabled || {};
@@ -21336,6 +21698,7 @@ window.removeProvider = function(idx) {
21336
21698
 
21337
21699
  window.addModel = function() {
21338
21700
  window.openSubWin(t('model.addModel'), '<div class="auto-input-group"><label>' + esc(t('model.modelName')) + '</label><input id="new-model-name" placeholder="gpt-4"></div>' +
21701
+ '<div class="auto-input-group"><label>' + esc(t('model.displayName')) + '</label><input id="new-model-display"></div>' +
21339
21702
  '<div class="auto-input-group"><label>' + esc(t('model.provider')) + '</label><select id="new-model-provider">' + state.providers.map(function(p,i){return '<option value="'+i+'">'+esc(p.name)+'</option>';}).join('') + '</select></div>' +
21340
21703
  '<div class="auto-input-group"><label>' + esc(t('model.contextSize')) + '</label><input id="new-model-ctx" type="number" value="128000" placeholder="tokens"></div>' +
21341
21704
  '<div style="display:flex;gap:10px;margin-bottom:10px;"><label style="font-size:11px;color:var(--text-dim);"><input type="checkbox" id="new-model-vision"> ' + esc(t('model.vision')) + '</label>' +
@@ -21367,7 +21730,8 @@ window.serializeThinkingTierMap = function(map) {
21367
21730
  };
21368
21731
 
21369
21732
  window.saveNewModel = function() {
21370
- var name = document.getElementById('new-model-name').value;
21733
+ var name = document.getElementById('new-model-name').value.trim();
21734
+ var display = document.getElementById('new-model-display').value.trim();
21371
21735
  var provIdx = parseInt(document.getElementById('new-model-provider').value);
21372
21736
  var ctxEl = document.getElementById('new-model-ctx');
21373
21737
  var visionEl = document.getElementById('new-model-vision');
@@ -21376,10 +21740,11 @@ window.saveNewModel = function() {
21376
21740
  var tierMapEl = document.getElementById('new-model-tier-map');
21377
21741
  if (name && !isNaN(provIdx) && state.providers[provIdx]) {
21378
21742
  if (!state.providers[provIdx].models) state.providers[provIdx].models = [];
21743
+ if (state.providers[provIdx].models.some(function(m) { return String(typeof m === 'string' ? m : m.name).toLowerCase() === name.toLowerCase(); })) { showUiNotice('该供应商下模型标识已存在', 'error', 'duplicate-model'); return; }
21379
21744
  var tierMap = window.parseThinkingTierMap(tierMapEl ? tierMapEl.value : '');
21380
21745
  var entry = {
21381
21746
  name: name,
21382
- display: name,
21747
+ display: display || name,
21383
21748
  description: descEl ? descEl.value : '',
21384
21749
  max_tokens: ctxEl ? parseInt(ctxEl.value || '128000') : 128000,
21385
21750
  vision: !!(visionEl && visionEl.checked),
@@ -21410,8 +21775,10 @@ window.editModel = function(provIdx, modelIdx) {
21410
21775
  var model = provider.models[modelIdx];
21411
21776
  if (!model) return;
21412
21777
  if (typeof model === 'string') model = { name: model, display: model };
21778
+ state.modelEditIdentity = { providerId: provider.id, modelName: model.name };
21413
21779
  window.openSubWin(t('common.edit'), '<div class="auto-input-group"><label>' + esc(t('model.modelName')) + '</label><input id="edit-model-name" value="' + escAttr(model.name || '') + '"></div>' +
21414
- '<div class="auto-input-group"><label>' + esc(t('model.provider')) + '</label><select id="edit-model-provider">' + state.providers.map(function(p,i){return '<option value="'+i+'"' + (i === provIdx ? ' selected' : '') + '>'+esc(p.name)+'</option>';}).join('') + '</select></div>' +
21780
+ '<div class="auto-input-group"><label>' + esc(t('model.displayName')) + '</label><input id="edit-model-display" value="' + escAttr(model.display || '') + '"></div>' +
21781
+ '<div class="auto-input-group"><label>' + esc(t('model.provider')) + '</label><select id="edit-model-provider" disabled>' + state.providers.map(function(p,i){return '<option value="'+i+'"' + (i === provIdx ? ' selected' : '') + '>'+esc(p.name)+'</option>';}).join('') + '</select></div>' +
21415
21782
  '<div class="auto-input-group"><label>' + esc(t('model.contextSize')) + '</label><input id="edit-model-ctx" type="number" value="' + escAttr(model.max_tokens || 4096) + '"></div>' +
21416
21783
  '<div style="display:flex;gap:10px;margin-bottom:10px;"><label style="font-size:11px;color:var(--text-dim);"><input type="checkbox" id="edit-model-vision"' + (model.vision ? ' checked' : '') + '> ' + esc(t('model.vision')) + '</label>' +
21417
21784
  '<label style="font-size:11px;color:var(--text-dim);"><input type="checkbox" id="edit-model-thinking"' + (model.thinking ? ' checked' : '') + '> ' + esc(t('model.thinking')) + '</label></div>' +
@@ -21422,7 +21789,11 @@ window.editModel = function(provIdx, modelIdx) {
21422
21789
  };
21423
21790
 
21424
21791
  window.saveModelEdit = function(oldProvIdx, modelIdx) {
21792
+ var identity = state.modelEditIdentity;
21793
+ if (!identity) return;
21794
+ oldProvIdx = state.providers.findIndex(function(p) { return p.id === identity.providerId; });
21425
21795
  var oldProvider = state.providers[oldProvIdx];
21796
+ modelIdx = oldProvider && oldProvider.models ? oldProvider.models.findIndex(function(m) { return (typeof m === 'string' ? m : m.name) === identity.modelName; }) : -1;
21426
21797
  if (!oldProvider || !oldProvider.models || !oldProvider.models[modelIdx]) return;
21427
21798
  var nameEl = document.getElementById('edit-model-name');
21428
21799
  var provEl = document.getElementById('edit-model-provider');
@@ -21432,15 +21803,17 @@ window.saveModelEdit = function(oldProvIdx, modelIdx) {
21432
21803
  var descEl = document.getElementById('edit-model-desc');
21433
21804
  var tierMapEl = document.getElementById('edit-model-tier-map');
21434
21805
  var name = nameEl ? nameEl.value.trim() : '';
21435
- var newProvIdx = provEl ? parseInt(provEl.value) : oldProvIdx;
21806
+ var newProvIdx = oldProvIdx;
21807
+ var displayEl = document.getElementById('edit-model-display');
21436
21808
  if (!name || isNaN(newProvIdx) || !state.providers[newProvIdx]) return;
21809
+ if (oldProvider.models.some(function(m, i) { return i !== modelIdx && String(typeof m === 'string' ? m : m.name).toLowerCase() === name.toLowerCase(); })) { showUiNotice('该供应商下模型标识已存在', 'error', 'duplicate-model'); return; }
21437
21810
  var previous = oldProvider.models[modelIdx];
21438
21811
  if (typeof previous === 'string') previous = { name: previous, display: previous };
21439
21812
  var tierMap = window.parseThinkingTierMap(tierMapEl ? tierMapEl.value : '');
21440
21813
  var updated = Object.assign({}, previous, {
21441
21814
  _previous_name: previous.name || name,
21442
21815
  name: name,
21443
- display: name,
21816
+ display: displayEl ? displayEl.value.trim() || name : name,
21444
21817
  description: descEl ? descEl.value : '',
21445
21818
  max_tokens: ctxEl ? parseInt(ctxEl.value || '4096') : 4096,
21446
21819
  vision: !!(visionEl && visionEl.checked),
@@ -24611,6 +24984,34 @@ window.activateConversationWithGlass = function(idx, row) {
24611
24984
  } });
24612
24985
  };
24613
24986
 
24987
+ /**
24988
+ * dev-0.6.6 hotfix: 拖动排序期间,行的顺序归用户所有。
24989
+ *
24990
+ * 拖动过程中任何权威列表推送(重命名、跑马灯状态、其他窗口或移动端)都会走到
24991
+ * renderConversations()。旧实现无条件把行重排回 state 顺序,用户刚刚让出来的落点
24992
+ * 会被抹掉,松手时提交的就是被重置后的位置——「拖到哪就落在哪」因此失效。
24993
+ * 拖动期间以 DOM 现有顺序为准,只把刚出现的新行按 state 位置插进去。
24994
+ */
24995
+ function conversationRowOrderDuringDrag(orderedRows, list) {
24996
+ var dragRow = conversationNativeDrag.row;
24997
+ if (!dragRow || dragRow.parentNode !== list) return orderedRows;
24998
+ var domRows = Array.prototype.slice.call(list.querySelectorAll('.conv-item'));
24999
+ var stateIndex = new Map();
25000
+ orderedRows.forEach(function(row, index) { stateIndex.set(row, index); });
25001
+ var sequence = domRows.filter(function(row) { return stateIndex.has(row); });
25002
+ if (!sequence.length) return orderedRows;
25003
+ orderedRows.forEach(function(row) {
25004
+ if (sequence.indexOf(row) >= 0) return;
25005
+ var index = stateIndex.get(row);
25006
+ var insertAt = sequence.length;
25007
+ for (var i = 0; i < sequence.length; i++) {
25008
+ if (stateIndex.get(sequence[i]) > index) { insertAt = i; break; }
25009
+ }
25010
+ sequence.splice(insertAt, 0, row);
25011
+ });
25012
+ return sequence;
25013
+ }
25014
+
24614
25015
  function renderConversations() {
24615
25016
  var list = els['conversation-list'];
24616
25017
  if (!list) return;
@@ -24771,7 +25172,8 @@ function renderConversations() {
24771
25172
  var stale = existingRows[id];
24772
25173
  if (stale && stale.parentNode === list) stale.parentNode.removeChild(stale);
24773
25174
  });
24774
- orderedRows.forEach(function(row, idx) {
25175
+ var rowsToPlace = conversationRowOrderDuringDrag(orderedRows, list);
25176
+ rowsToPlace.forEach(function(row, idx) {
24775
25177
  var current = list.children[idx];
24776
25178
  if (current === row) return;
24777
25179
  list.insertBefore(row, current || null);
@@ -24860,6 +25262,7 @@ window.newConversation = function(workspaceReference, branchCommunication) {
24860
25262
  state._promptBoundKey = conversationDraftKey();
24861
25263
  renderConversations();
24862
25264
  els['chat-area'].innerHTML = '';
25265
+ state.renderedChatTargetKey = '';
24863
25266
  var target = currentConversationTarget(id);
24864
25267
  var modelRevisionAtActivation = Number(state.modelSelectionRevision || 0);
24865
25268
  var activation = api.activateConversation ? api.activateConversation(target) : (api.ensureConversation ? api.ensureConversation(target) : Promise.resolve(null));
@@ -24947,6 +25350,11 @@ window.switchConversation = function(idx) {
24947
25350
  setActiveWorkspaceConversation(idx);
24948
25351
  var activeId = activeConversationId();
24949
25352
  var activeBrowserTarget = currentConversationTarget(activeId);
25353
+ // Rebind the visible composer to the selected conversation before any async
25354
+ // backend hydration. Users can type as soon as the row is selected; leaving
25355
+ // _promptBoundKey on the previous conversation would persist that input to
25356
+ // the wrong target while syncBackendConversation is still in flight.
25357
+ restoreConversationDraft(activeBrowserTarget);
24950
25358
  var activeBrowserView = browserGuestForTarget(activeBrowserTarget);
24951
25359
  if (activeBrowserView) setActiveBrowserGuest(activeBrowserView, activeBrowserTarget);
24952
25360
  else if (state.rightTab === 'browser') window.ensureBrowserPanel({ target: activeBrowserTarget, activate: true }).catch(function(error) {
@@ -25073,6 +25481,7 @@ window.archiveConv = function(conversationId) {
25073
25481
  if (priorActiveId === targetId) {
25074
25482
  state.activeBackendConversationId = '';
25075
25483
  if (els['chat-area']) els['chat-area'].innerHTML = '';
25484
+ state.renderedChatTargetKey = '';
25076
25485
  if (window.renderFlowTakeover) window.renderFlowTakeover(false, '', { target: targetRuntime });
25077
25486
  }
25078
25487
  } else {
@@ -25085,6 +25494,7 @@ window.archiveConv = function(conversationId) {
25085
25494
  if (priorActiveId === targetId) {
25086
25495
  state.activeBackendConversationId = nextActiveId;
25087
25496
  if (els['chat-area']) els['chat-area'].innerHTML = '';
25497
+ state.renderedChatTargetKey = '';
25088
25498
  }
25089
25499
  }
25090
25500
  if (state.flowTakeovers) {
@@ -26964,7 +27374,15 @@ window.commitConversationDomOrder = function() {
26964
27374
  var ids = Array.from(list.querySelectorAll('.conv-item')).map(function(row) {
26965
27375
  return String(row.getAttribute('data-conversation-id') || '');
26966
27376
  }).filter(function(id) { return !!byId[id]; });
26967
- if (ids.length !== convs.length) return renderConversations();
27377
+ if (!ids.length) return renderConversations();
27378
+ // dev-0.6.6 hotfix: 拖动期间权威列表可能新增/删除会话(重命名、其他窗口、
27379
+ // 移动端)。旧实现一旦发现行数与 state 不一致就整表重渲染,用户刚拖出来的落点
27380
+ // 会被丢弃。这里以 DOM 顺序为准,把没有出现在 DOM 里的会话按 state 顺序补在末尾,
27381
+ // 保证提交的永远是一份完整、且以用户拖动结果为准的顺序。
27382
+ var missingIds = convs
27383
+ .filter(function(item) { return ids.indexOf(String(item.id)) < 0; })
27384
+ .map(function(item) { return String(item.id); });
27385
+ ids = ids.concat(missingIds);
26968
27386
  var activeId = activeConversationId();
26969
27387
  convs.splice.apply(convs, [0, convs.length].concat(ids.map(function(id) { return byId[id]; })));
26970
27388
  setActiveWorkspaceConversationById(activeId);
@@ -28439,7 +28857,6 @@ function schedulePostStartupUiRendering() {
28439
28857
  if (Array.isArray(s.wslDistros)) state.wslDistros = s.wslDistros;
28440
28858
  if (s.conversationId) state.activeBackendConversationId = s.conversationId;
28441
28859
  if (s.conversationPlan) state.conversationPlan = normalizeConversationPlan(s.conversationPlan);
28442
- applyAutoRouteRatingState(s);
28443
28860
  if (s.chatMessages) {
28444
28861
  hydrateConversationBranchState(s);
28445
28862
  renderChatMessages(s.chatMessages);
@@ -28447,10 +28864,11 @@ function schedulePostStartupUiRendering() {
28447
28864
  if (s.defaultFlow) state.defaultFlow = s.defaultFlow;
28448
28865
  if (s.autoSwitch !== undefined) state.autoSwitch = s.autoSwitch ? 'on' : 'off';
28449
28866
  if (s.autoSwitchScope) state.autoSwitchScope = s.autoSwitchScope === 'provider' ? 'provider' : 'all';
28867
+ window.applyJevDecisionModelState(s);
28450
28868
  if (s.openAIApiMode) state.openAIApiMode = (s.openAIApiMode === 'chat' || s.openAIApiMode === 'responses') ? s.openAIApiMode : 'chat_stream';
28451
28869
  if (s.fallbackOnUnavailable !== undefined) state.fallbackOnUnavailable = s.fallbackOnUnavailable ? 'on' : 'off';
28452
28870
  if (s.switchTendency) {
28453
- state.switchTendency = s.switchTendency === 'performance' ? 'quality' : (s.switchTendency === 'cheap_save' ? 'cost' : (s.switchTendency === 'default' ? 'balanced' : s.switchTendency));
28871
+ state.switchTendency = ['conservative', 'balanced', 'aggressive'].indexOf(s.switchTendency) >= 0 ? s.switchTendency : 'balanced';
28454
28872
  }
28455
28873
  if (s.providers) {
28456
28874
  state.providers = s.providers;