newmark-agent 0.6.3 → 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 (72) 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 +3046 -1038
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +149 -12
  8. package/dist/core/agent.js +843 -176
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +171 -22
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/branchIdentity.d.ts +29 -0
  14. package/dist/core/branchIdentity.js +54 -0
  15. package/dist/core/config.js +27 -11
  16. package/dist/core/continuation/contracts.d.ts +1 -1
  17. package/dist/core/continuation/store.d.ts +119 -1
  18. package/dist/core/continuation/store.js +337 -19
  19. package/dist/core/conversationKernel.d.ts +62 -8
  20. package/dist/core/conversationKernel.js +361 -47
  21. package/dist/core/conversationStateDocument.d.ts +18 -0
  22. package/dist/core/conversationStateDocument.js +88 -0
  23. package/dist/core/conversationVisualBudget.d.ts +74 -0
  24. package/dist/core/conversationVisualBudget.js +153 -0
  25. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  26. package/dist/core/electronUtilityAgentClient.js +32 -8
  27. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  28. package/dist/core/electronUtilityRuntimePool.js +93 -24
  29. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  30. package/dist/core/hostRuntimeHooks.js +17 -0
  31. package/dist/core/jevDecision.d.ts +144 -0
  32. package/dist/core/jevDecision.js +226 -0
  33. package/dist/core/memoryProbe.d.ts +24 -0
  34. package/dist/core/memoryProbe.js +104 -0
  35. package/dist/core/mobilePairing.d.ts +7 -1
  36. package/dist/core/mobilePairing.js +9 -1
  37. package/dist/core/performanceDiagnostics.d.ts +1 -1
  38. package/dist/core/routeDecisionValidator.d.ts +86 -0
  39. package/dist/core/routeDecisionValidator.js +249 -0
  40. package/dist/core/routeEligibility.d.ts +52 -0
  41. package/dist/core/routeEligibility.js +108 -0
  42. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  43. package/dist/core/runtimeMemoryBudget.js +12 -0
  44. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  45. package/dist/core/utilityHostToolRouter.js +2 -0
  46. package/dist/core/visualDownscale.d.ts +6 -0
  47. package/dist/core/visualDownscale.js +137 -0
  48. package/dist/core/wslAgentClient.d.ts +1 -2
  49. package/dist/core/wslAgentClient.js +0 -8
  50. package/dist/core/wslAgentProtocol.d.ts +1 -10
  51. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  52. package/dist/core/wslAgentRuntimePool.js +17 -22
  53. package/dist/llm/provider.d.ts +3 -1
  54. package/dist/llm/provider.js +53 -9
  55. package/dist/main.js +108 -31
  56. package/dist/preload.js +3 -1
  57. package/dist/server.js +30 -18
  58. package/dist/tools/computerUse.d.ts +4 -0
  59. package/dist/tools/computerUse.js +202 -4
  60. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  61. package/dist/tools/computerUsePowerShellHost.js +105 -31
  62. package/dist/tools/index.js +4 -1
  63. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  64. package/dist/tui/src/i18n.js +12 -1
  65. package/dist/tui/src/render.js +9 -2
  66. package/dist/tui/src/settings-schema.js +19 -0
  67. package/dist/tui/src/state.js +69 -1
  68. package/dist/ui/index.html +994 -277
  69. package/dist/ui/lucide-sprite.svg +0 -8
  70. package/dist/wsl-agent-host.bundle.cjs +2851 -1013
  71. package/dist/wsl-agent-host.js +0 -3
  72. package/package.json +38 -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;
@@ -3373,6 +3396,19 @@ button.todo-item { background: transparent; }
3373
3396
  .queue-item.dragging { opacity: 0.58; transform: scale(0.99); }
3374
3397
  .queue-item.queue-drag-displaced { transition: transform 120ms var(--ease-out-expo); }
3375
3398
 
3399
+ /* dev-0.6.4: 该排队行绑定的是另一条分页,正在等待自己的运行分支。 */
3400
+ .queue-item[data-queue-branch-waiting="1"] { opacity: 0.68; }
3401
+ .queue-item[data-queue-branch-waiting="1"] .queue-edit { font-style: italic; }
3402
+
3403
+ /* dev-0.6.4: 该排队行被失败的前置 Build 阻断,恢复队列会重新挂载到已提交 frontier。 */
3404
+ .queue-item[data-queue-blocked="1"] .queue-edit { color: var(--danger, #e06c75); }
3405
+ .queue-item[data-queue-blocked="1"]::after {
3406
+ content: '!';
3407
+ color: var(--danger, #e06c75);
3408
+ font: 10px var(--font-mono);
3409
+ }
3410
+
3411
+
3376
3412
  .queue-drag {
3377
3413
  width: 22px;
3378
3414
  height: 22px;
@@ -3796,6 +3832,18 @@ button.todo-item { background: transparent; }
3796
3832
  position: relative;
3797
3833
  z-index: 1;
3798
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
+ }
3799
3847
  @keyframes model-menu-in { from { opacity: 0; transform: translateY(5px) scale(.985); } to { opacity: 1; transform: none; } }
3800
3848
 
3801
3849
  .newmark-select-shell {
@@ -3904,6 +3952,9 @@ button.todo-item { background: transparent; }
3904
3952
 
3905
3953
  .tool-select:hover { border-color: var(--border-hover); }
3906
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); }
3907
3958
 
3908
3959
  .context-token-wrap {
3909
3960
  position: relative;
@@ -3967,6 +4018,20 @@ button.todo-item { background: transparent; }
3967
4018
  opacity: 1;
3968
4019
  transform: scale(1.08);
3969
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
+ }
3970
4035
 
3971
4036
  #context-token-tooltip {
3972
4037
  display: none;
@@ -4013,9 +4078,9 @@ button.todo-item { background: transparent; }
4013
4078
  transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo);
4014
4079
  }
4015
4080
 
4016
- .mode-toggle-btn:hover { background: rgba(255,255,255,0.05); color: var(--text); }
4017
- .mode-toggle-btn.active { background: rgba(91,120,255,0.15); color: var(--accent); border-radius: var(--radius-full); }
4018
- .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); }
4019
4084
 
4020
4085
  .workspace-type-pager {
4021
4086
  display: grid;
@@ -4046,11 +4111,11 @@ button.todo-item { background: transparent; }
4046
4111
  display: flex;
4047
4112
  align-items: center;
4048
4113
  justify-content: center;
4049
- 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);
4050
4115
  box-shadow: 0 2px 12px var(--accent-glow);
4051
4116
  }
4052
4117
 
4053
- #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); }
4054
4119
  #submit-btn:active { transform: scale(0.92); transition: transform 80ms var(--ease-out-expo); }
4055
4120
  #submit-btn::after { display: none !important; }
4056
4121
  #submit-btn:active { transform: none; scale: 1; }
@@ -4176,13 +4241,13 @@ button.todo-item { background: transparent; }
4176
4241
 
4177
4242
  .working-glow {
4178
4243
  animation: none;
4179
- box-shadow: 0 0 16px var(--accent-glow), 0 0 40px rgba(91,120,255,0.1);
4180
- 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;
4181
4246
  }
4182
4247
 
4183
4248
  @keyframes working-border-glow {
4184
- 0%, 100% { box-shadow: 0 0 12px var(--accent-glow), 0 0 30px rgba(91,120,255,0.08); }
4185
- 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); }
4186
4251
  }
4187
4252
 
4188
4253
  /* ============================================================
@@ -4443,8 +4508,8 @@ button.todo-item { background: transparent; }
4443
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);
4444
4509
  }
4445
4510
 
4446
- .stab-btn:hover { background: rgba(255,255,255,0.05); color: var(--text); }
4447
- .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); }
4448
4513
  .stab-btn:focus-visible { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-glow); color:var(--text-bright); }
4449
4514
 
4450
4515
  .plugin-tabs { overflow-x:auto; scrollbar-width:thin; }
@@ -4760,6 +4825,10 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4760
4825
  box-shadow: 0 2px 6px rgba(0,0,0,.3);
4761
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);
4762
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
+ }
4763
4832
  #settings-glass-opacity.liquid-range-dragging::-webkit-slider-thumb {
4764
4833
  width: 26px;
4765
4834
  height: 20px;
@@ -5097,9 +5166,9 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5097
5166
  .memory-lab-node.selected {
5098
5167
  min-height: 52px;
5099
5168
  border-color: var(--accent);
5100
- background: rgba(91,120,255,0.16);
5169
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
5101
5170
  color: var(--text-bright);
5102
- 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);
5103
5172
  }
5104
5173
  .memory-lab-node-label {
5105
5174
  min-width: 0;
@@ -5303,8 +5372,8 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
5303
5372
  }
5304
5373
  .memory-lab-overview-node.focus {
5305
5374
  border-color: var(--accent);
5306
- background: rgba(91, 120, 255, 0.2);
5307
- 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);
5308
5377
  }
5309
5378
  .memory-lab-overview-node.dim { opacity: 0.18; }
5310
5379
  .memory-lab-overview-stage.zoom-dots .memory-lab-overview-node {
@@ -5409,9 +5478,9 @@ html.newmark-memory-overview-viewer body {
5409
5478
  }
5410
5479
  [data-theme="light"] .memory-lab-node.selected,
5411
5480
  [data-theme="light"] .memory-lab-overview-node.focus {
5412
- background: rgba(91,120,255,0.1);
5481
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
5413
5482
  color: var(--text-bright);
5414
- 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);
5415
5484
  }
5416
5485
  [data-theme="light"] .memory-lab-overview-grid {
5417
5486
  opacity: 0.34;
@@ -5429,15 +5498,15 @@ html.newmark-memory-overview-viewer body {
5429
5498
  .model-chip {
5430
5499
  display:flex; align-items:center; gap:12px; box-sizing:border-box;
5431
5500
  width:100%; min-width:0; margin:6px 0; padding:10px 12px;
5432
- border-radius:var(--radius-sm); background:rgba(91,120,255,0.1);
5433
- 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);
5434
5503
  }
5435
5504
  .model-chip-info { flex:1; min-width:0; text-align:left; overflow-wrap:anywhere; }
5436
5505
  .model-chip-actions { display:flex; flex:0 0 auto; align-items:center; gap:6px; margin-left:auto; }
5437
5506
  .model-response-warning { color:var(--text-dim); font-size:inherit; margin-left:4px; font-weight:400; }
5438
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; }
5439
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); }
5440
- .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); }
5441
5510
  .model-enable-switch[aria-checked="true"] .model-enable-thumb { transform:translateX(16px); }
5442
5511
  .model-enable-switch:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
5443
5512
  .model-enable-switch:disabled { opacity:.6; }
@@ -5511,7 +5580,7 @@ html.newmark-memory-overview-viewer body {
5511
5580
  padding: 8px 20px;
5512
5581
  border: 1px solid var(--accent);
5513
5582
  border-radius: var(--radius-md);
5514
- background: rgba(91,120,255,0.1);
5583
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
5515
5584
  color: var(--accent);
5516
5585
  font-size: 12px;
5517
5586
  font-family: var(--font-ui);
@@ -5519,7 +5588,7 @@ html.newmark-memory-overview-viewer body {
5519
5588
  transition: background var(--duration-fast) var(--ease-out-expo);
5520
5589
  }
5521
5590
 
5522
- .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); }
5523
5592
 
5524
5593
  /* Archive list */
5525
5594
  .archive-item {
@@ -5716,14 +5785,14 @@ html.newmark-memory-overview-viewer body {
5716
5785
  transition: background var(--duration-fast) var(--ease-out-expo), color var(--duration-fast) var(--ease-out-expo);
5717
5786
  }
5718
5787
 
5719
- .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); }
5720
5789
 
5721
5790
  /* ============================================================
5722
5791
  ANIMATIONS & UTILITY
5723
5792
  ============================================================ */
5724
5793
  @keyframes fade-in-up {
5725
- from { opacity: 0; transform: translateY(12px) translateZ(0); }
5726
- to { opacity: 1; transform: translateY(0) translateZ(0); }
5794
+ from { opacity: 0; transform: translateY(12px); }
5795
+ to { opacity: 1; transform: translateY(0); }
5727
5796
  }
5728
5797
 
5729
5798
  .hidden { display: none !important; }
@@ -5949,8 +6018,12 @@ html.newmark-memory-overview-viewer body {
5949
6018
  .liquid-glass-popup,
5950
6019
  .liquid-glass-popup.liquid-popup-exit,
5951
6020
  .liquid-menu-color-block,
5952
- .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; }
5953
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; }
5954
6027
  }
5955
6028
  @media (max-width: 900px), (max-height: 680px) {
5956
6029
  .liquid-glass-popup { --liquid-popup-blur: calc(var(--glass-blur-3) * 3); }
@@ -6045,7 +6118,7 @@ html.newmark-memory-overview-viewer body {
6045
6118
  }
6046
6119
  .liquid-glass-popup[data-liquid-popup-feedback='outward']::after { opacity: 0; }
6047
6120
  @media (prefers-reduced-motion: reduce) {
6048
- .liquid-popup-feedback-surface { transition-duration: 1ms !important; }
6121
+ .liquid-popup-feedback-surface { transition-duration: 0.01ms !important; }
6049
6122
  }
6050
6123
  .model-select-menu-option,
6051
6124
  .newmark-select-option,
@@ -6155,10 +6228,12 @@ button::after {
6155
6228
  border-radius: inherit;
6156
6229
  opacity: 0;
6157
6230
  background: var(--liquid-refraction);
6158
- backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
6159
- -webkit-backdrop-filter: blur(calc(var(--glass-blur-3) * 4)) saturate(145%) brightness(1.04);
6160
6231
  box-shadow: var(--liquid-top-highlight), var(--liquid-inner-shadow), 0 4px 14px rgba(0,0,0,0.18);
6161
- 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;
6162
6237
  }
6163
6238
  button:hover:not(:disabled) {
6164
6239
  filter: brightness(1.06);
@@ -6169,7 +6244,12 @@ button:active:not(:disabled) {
6169
6244
  background-color: transparent !important;
6170
6245
  box-shadow: none;
6171
6246
  }
6172
- 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
+ }
6173
6253
  button:disabled { opacity: 0.5; }
6174
6254
 
6175
6255
  /* Unified glass controls are rendered by a real top-layer float. Their local
@@ -6207,6 +6287,21 @@ button.conversation-work-run-head::after,
6207
6287
  button.shell-block-header::after,
6208
6288
  button.diff-block-header::after,
6209
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; }
6210
6305
  button.conversation-work-run-head:active:not(:disabled),
6211
6306
  button.shell-block-header:active:not(:disabled),
6212
6307
  button.diff-block-header:active:not(:disabled),
@@ -6707,14 +6802,6 @@ input[type="checkbox"].liquid-switch-dragging::before {
6707
6802
  <path d="M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1" />
6708
6803
  <path d="M9 6v12" />
6709
6804
  </symbol>
6710
- <symbol id="thumbs-down" viewBox="0 0 24 24">
6711
- <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" />
6712
- <path d="M17 14V2" />
6713
- </symbol>
6714
- <symbol id="thumbs-up" viewBox="0 0 24 24">
6715
- <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" />
6716
- <path d="M7 10v12" />
6717
- </symbol>
6718
6805
  <symbol id="trash-2" viewBox="0 0 24 24">
6719
6806
  <path d="M10 11v6" />
6720
6807
  <path d="M14 11v6" />
@@ -7195,12 +7282,15 @@ var state = {
7195
7282
  feedbackLevel: 'default',
7196
7283
  autoSwitch: 'off',
7197
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,
7198
7291
  openAIApiMode: 'chat_stream',
7199
7292
  fallbackOnUnavailable: 'off',
7200
7293
  switchTendency: 'balanced',
7201
- autoRouteRatingAvailable: false,
7202
- autoRouteRatingRouteId: '',
7203
- autoRouteRatingScore: 0,
7204
7294
  workspaces: [],
7205
7295
  currentWorkspace: '',
7206
7296
  currentWorkspaceId: '',
@@ -7211,6 +7301,12 @@ var state = {
7211
7301
  pendingOptions: [],
7212
7302
  pendingOptionAnswers: {},
7213
7303
  pendingOptionBatchKey: '',
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: '',
7309
+ pendingOptionAnswersByTarget: {},
7214
7310
  promptAttachments: [],
7215
7311
  nextQueue: [],
7216
7312
  nextQueueRequests: [],
@@ -7221,6 +7317,7 @@ var state = {
7221
7317
  backendQueue: { steering: [], followUp: [] },
7222
7318
  backendQueuesByTarget: {},
7223
7319
  queueItemsByTarget: {},
7320
+ pendingDrainRefreshByTarget: {},
7224
7321
  defaultFlow: '',
7225
7322
  settingsActiveTab: 'general',
7226
7323
  globalAgentPrompt: '',
@@ -7575,7 +7672,8 @@ var NEWMARK_I18N = {
7575
7672
  'model.githubModelsImported': 'Imported {count} models.',
7576
7673
  'model.githubModelsWarning': 'Warning: {warning}',
7577
7674
  'model.autoDetect': 'Auto detect',
7578
- 'model.modelName': 'Model name',
7675
+ 'model.modelName': 'API model identifier',
7676
+ 'model.displayName': 'Display name',
7579
7677
  'model.provider': 'Provider',
7580
7678
  'model.contextSize': 'Context size',
7581
7679
  'model.vision': 'Vision',
@@ -7597,15 +7695,21 @@ var NEWMARK_I18N = {
7597
7695
  'model.openAIChat': 'Chat Completions',
7598
7696
  'model.openAIResponses': 'Responses API',
7599
7697
  'model.fallback': 'Fallback unavailable model',
7600
- '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.',
7601
7700
  'model.costSaving': 'Cost saving',
7602
7701
  'model.performance': 'Quality',
7603
- 'model.qualityPolicy': 'Quality · 0% loss band',
7604
- 'model.balancedPolicy': 'Balanced · 2% loss band',
7605
- 'model.costPolicy': 'Cost · 6% loss band',
7606
- 'model.speedPolicy': 'Speed · 4% loss band',
7607
- 'model.learnedPreference': 'Adaptive preference',
7608
- '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.',
7609
7713
  'model.validationTitle': 'Model validation',
7610
7714
  'model.validating': 'Validating model latency and capabilities...',
7611
7715
  'model.validationProgress': 'Validation progress',
@@ -8318,7 +8422,8 @@ var NEWMARK_I18N = {
8318
8422
  'model.githubModelsImported': '已导入 {count} 个模型。',
8319
8423
  'model.githubModelsWarning': '警告:{warning}',
8320
8424
  'model.autoDetect': '自动检测',
8321
- 'model.modelName': '模型名称',
8425
+ 'model.modelName': '调用模型标识',
8426
+ 'model.displayName': '显示名称',
8322
8427
  'model.provider': '供应商',
8323
8428
  'model.contextSize': '上下文规模',
8324
8429
  'model.vision': '视觉',
@@ -8340,15 +8445,21 @@ var NEWMARK_I18N = {
8340
8445
  'model.openAIChat': 'Chat Completions',
8341
8446
  'model.openAIResponses': 'Responses API',
8342
8447
  'model.fallback': '模型不可用时回退',
8343
- 'model.preference': '切换倾向',
8448
+ 'model.preference': '模型决策倾向',
8449
+ 'model.preferenceDesc': '每次 Auto 选择均由当前符合条件的模型决策。此设置只影响模型的决策倾向,不替模型选择部署,也不设数值阈值。',
8344
8450
  'model.costSaving': '节省优先',
8345
8451
  'model.performance': '质量优先',
8346
- 'model.qualityPolicy': '质量 · 0% 损失带',
8347
- 'model.balancedPolicy': '均衡 · 2% 损失带',
8348
- 'model.costPolicy': '成本 · 6% 损失带',
8349
- 'model.speedPolicy': '速度 · 4% 损失带',
8350
- 'model.learnedPreference': '自适应偏好',
8351
- '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 决策策略未做任何修改。',
8352
8463
  'model.validationTitle': '模型校验',
8353
8464
  'model.validating': '正在校验模型延迟和能力...',
8354
8465
  'model.validationProgress': '校验进度',
@@ -9636,87 +9747,6 @@ window.copyMessageText = async function(button) {
9636
9747
  var branchApiOverride = null;
9637
9748
  window.__setBranchApiForTest = function(nextApi) { branchApiOverride = nextApi || null; };
9638
9749
 
9639
- function applyAutoRouteRatingState(snapshot) {
9640
- snapshot = snapshot || {};
9641
- var decision = snapshot.routeDecision && typeof snapshot.routeDecision === 'object' ? snapshot.routeDecision : null;
9642
- var routeId = decision ? String(decision.routeId || '') : '';
9643
- var selection = decision && decision.requestedSelection;
9644
- var available = !!routeId && !!decision.resolvedDeployment && selection && selection.kind === 'auto'
9645
- && snapshot.autoRouteRatingAvailable !== false;
9646
- if (routeId !== state.autoRouteRatingRouteId) state.autoRouteRatingScore = 0;
9647
- state.autoRouteRatingAvailable = !!available;
9648
- state.autoRouteRatingRouteId = available ? routeId : '';
9649
- if (!available) state.autoRouteRatingScore = 0;
9650
- }
9651
-
9652
- function autoRouteRatingLabel(score) {
9653
- if (currentLang() === 'zh') return score > 0 ? '此 Auto 路由有帮助' : '此 Auto 路由没有帮助';
9654
- return score > 0 ? 'This Auto route was helpful' : 'This Auto route was not helpful';
9655
- }
9656
-
9657
- function attachAutoRouteRatingControls(message) {
9658
- if (!message || !api.rateAutoRoute || !state.autoRouteRatingAvailable || !state.autoRouteRatingRouteId) return;
9659
- var actions = message.querySelector('.msg-actions');
9660
- if (!actions) return;
9661
- var old = actions.querySelectorAll('.route-rating-btn');
9662
- for (var i = 0; i < old.length; i++) old[i].remove();
9663
- [1, -1].forEach(function(score) {
9664
- var label = autoRouteRatingLabel(score);
9665
- var selected = Number(state.autoRouteRatingScore || 0) === score;
9666
- var button = document.createElement('button');
9667
- button.type = 'button';
9668
- button.className = 'msg-action-btn route-rating-btn' + (selected ? ' route-rating-selected' : '');
9669
- button.title = label;
9670
- button.setAttribute('aria-label', label);
9671
- button.setAttribute('aria-pressed', selected ? 'true' : 'false');
9672
- button.setAttribute('data-route-id', state.autoRouteRatingRouteId);
9673
- button.innerHTML = iconSvg(score > 0 ? 'thumbs-up' : 'thumbs-down', label, 'tiny');
9674
- button.disabled = !!state.autoRouteRatingScore;
9675
- button.addEventListener('click', function() { window.rateAutoRoute(button, score); });
9676
- actions.appendChild(button);
9677
- });
9678
- }
9679
-
9680
- function renderAutoRouteRatingControls() {
9681
- if (!els['chat-area']) return;
9682
- var existing = els['chat-area'].querySelectorAll('.route-rating-btn');
9683
- for (var i = 0; i < existing.length; i++) existing[i].remove();
9684
- if (!state.autoRouteRatingAvailable) return;
9685
- var messages = els['chat-area'].querySelectorAll('.chat-msg.assistant:not(.work-run-message)');
9686
- attachAutoRouteRatingControls(messages.length ? messages[messages.length - 1] : null);
9687
- }
9688
-
9689
- window.rateAutoRoute = async function(button, score) {
9690
- if (!api.rateAutoRoute || !button) return;
9691
- var message = button.closest ? button.closest('.chat-msg') : null;
9692
- var buttons = message ? message.querySelectorAll('.route-rating-btn') : [];
9693
- for (var i = 0; i < buttons.length; i++) buttons[i].disabled = true;
9694
- var routeId = String(button.getAttribute('data-route-id') || '');
9695
- try {
9696
- var result = await api.rateAutoRoute({
9697
- target: currentConversationTarget(),
9698
- score: score,
9699
- routeId: routeId
9700
- });
9701
- if (result && (result.ok || result.reason === 'already_rated')) {
9702
- state.autoRouteRatingScore = Number(result.score) === -1 || Number(result.score) === 1 ? Number(result.score) : score;
9703
- renderAutoRouteRatingControls();
9704
- showUiNotice(currentLang() === 'zh' ? '已记录 Auto 路由反馈。' : 'Auto route feedback recorded.', 'info', 'auto-route-rating-' + routeId);
9705
- return;
9706
- }
9707
- for (var j = 0; j < buttons.length; j++) buttons[j].disabled = false;
9708
- var reason = result && result.reason ? String(result.reason) : 'no_active_auto_route';
9709
- showUiNotice(
9710
- currentLang() === 'zh' ? '无法记录 Auto 路由反馈:' + reason : 'Unable to record Auto route feedback: ' + reason,
9711
- 'error',
9712
- 'auto-route-rating-' + routeId
9713
- );
9714
- } catch (error) {
9715
- for (var k = 0; k < buttons.length; k++) buttons[k].disabled = false;
9716
- showUiNotice(error && error.message ? error.message : String(error), 'error', 'auto-route-rating-' + routeId);
9717
- }
9718
- };
9719
-
9720
9750
  window.editUserMessage = function(button, messageIndex) {
9721
9751
  var message = button && button.closest ? button.closest('.chat-msg') : null;
9722
9752
  if (!message || message.querySelector('.message-inline-editor')) return;
@@ -9954,8 +9984,12 @@ window.submitUserMessageEdit = async function(message, messageIndex, editedText,
9954
9984
  if (!isActiveConversationTarget(target)) return;
9955
9985
  clearAgentWorkEvents(conversationId);
9956
9986
  resetConversationWorkUi(conversationId, false);
9957
- state.backendQueue = setBackendQueueForTarget({ steering: [], followUp: [] }, target);
9958
- if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, target);
9987
+ // dev-0.6.4: 生成分页时绝不按新分支的清空队列覆盖本地投影。排队行是用户
9988
+ // 已经提交的输入,属于它受理时捕获的那条分支;旧逻辑在这里把队列整体
9989
+ // 清零,于是分页之后排队内容从界面上消失。内核的执行投影仍然只跟随当前
9990
+ // 激活分支(新分支绝不接管另一页的排队输入),刷新由切回该分页时的
9991
+ // 权威快照完成;这里只保留可见性并标记「等待所属分页」。
9992
+ if (result && Array.isArray(result.queueItems)) setQueueItemsForTarget(result.queueItems, target);
9959
9993
  state.conversationBranches = Array.isArray(result.branches) ? result.branches : [];
9960
9994
  state.conversationBranchGroups = Array.isArray(result.branchGroups) ? result.branchGroups : [];
9961
9995
  state.activeConversationBranchId = String(result.activeBranchId || '');
@@ -10165,7 +10199,8 @@ function appendConversationImageAttachments(container, input) {
10165
10199
  }
10166
10200
 
10167
10201
  function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10168
- 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']));
10169
10204
  text = redactSensitiveText(text);
10170
10205
  var owningRunId = messageMeta && String(messageMeta.runId || '');
10171
10206
  if (owningRunId && role === 'assistant' && els['chat-area']) {
@@ -10196,6 +10231,7 @@ function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10196
10231
  }
10197
10232
  var div = document.createElement('div');
10198
10233
  div.className = 'chat-msg ' + role;
10234
+ if (messageMeta && messageMeta.skipEntryAnimation) div.classList.add('history-static');
10199
10235
  var metaLabel = role === 'user'
10200
10236
  ? (String(mode || '').toLowerCase() === 'flow-user-input' ? t('flow.userInput') : t('message.user'))
10201
10237
  : (role === 'workflow' ? t('message.workflow') : (role === 'system' ? t('message.system') : t('message.agent')));
@@ -10212,7 +10248,11 @@ function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10212
10248
  if (model) metaInfo += (metaInfo ? ' | ' : '') + t('message.model') + ': ' + model;
10213
10249
  div.innerHTML = '<div class="meta"><span class="msg-role">' + metaLabel + '</span><span> | ' + esc(displayTimestamp) + '</span>' +
10214
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;
10215
10253
  div._newmarkMessageText = String(text || '');
10254
+ div._newmarkMessageMode = String(mode || '');
10255
+ div._newmarkMessageModel = String(model || '');
10216
10256
  div._newmarkMessageAttachments = appendConversationImageAttachments(div.querySelector('.msg-body'), messageMeta && messageMeta.attachments);
10217
10257
  div._newmarkDisplayImages = role === 'assistant' && owningRunId
10218
10258
  ? appendWorkDisplayImages(div.querySelector('.msg-body'), owningRunId, messageMeta && messageMeta.target)
@@ -10235,7 +10275,7 @@ function addMsg(role, text, mode, model, messageIndex, messageMeta) {
10235
10275
  else els['chat-area'].appendChild(div);
10236
10276
  } else els['chat-area'].appendChild(div);
10237
10277
  if (chatWasAtBottom) _chatShouldAutoScroll = true;
10238
- autoScrollIfAtBottom();
10278
+ if (!skipAutoScroll) autoScrollIfAtBottom();
10239
10279
  return div;
10240
10280
  }
10241
10281
 
@@ -10453,6 +10493,8 @@ function updateMsg(div, text, role, mode, model, options) {
10453
10493
  if (preserveRunFinalResponse) div.classList.add('run-final-response');
10454
10494
  text = redactSensitiveText(text);
10455
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 || '');
10456
10498
  var body = div.querySelector('.msg-body');
10457
10499
  var streaming = !!(options && options.streaming);
10458
10500
  if (body) {
@@ -10654,6 +10696,47 @@ function runtimeKeyFor(workspaceId, conversationId) {
10654
10696
  return state.runtimeKeyAliases && state.runtimeKeyAliases[base] || base;
10655
10697
  }
10656
10698
 
10699
+ // dev-0.6.4: 全对话 id(分支身份)。用户分页编辑产生的分支与实验性「分支交流」
10700
+ // 产生的分支共用这一条规则:`<conversationId>::branch:<branchNodeId>`。
10701
+ // 供应商会话序列 id 与所有分支级缓存都必须使用它;兄弟分支绝不共享消息、
10702
+ // Build 列表或折叠历史。
10703
+ function conversationBranchIdentity(conversationId, branchNodeId) {
10704
+ var conversation = String(conversationId || 'default') || 'default';
10705
+ var branch = String(branchNodeId || '');
10706
+ return branch ? conversation + '::branch:' + branch : conversation;
10707
+ }
10708
+
10709
+ // 记录每个会话最近一次「被浏览」的分支节点,供后台会话的缓存键使用。
10710
+ function rememberViewedBranchNodeId(target, branchNodeId) {
10711
+ var owner = normalizeQueuedConversationTarget(target);
10712
+ if (!owner) return '';
10713
+ if (!state.viewedBranchNodeIdsByTarget) state.viewedBranchNodeIdsByTarget = {};
10714
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
10715
+ var branch = String(branchNodeId || '');
10716
+ if (branch) state.viewedBranchNodeIdsByTarget[key] = branch;
10717
+ return String(state.viewedBranchNodeIdsByTarget[key] || '');
10718
+ }
10719
+
10720
+ function viewedBranchNodeIdFor(target) {
10721
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
10722
+ if (!owner) return '';
10723
+ if (isActiveConversationTarget(owner)) {
10724
+ var path = Array.isArray(state.viewedConversationBranchNodePath) ? state.viewedConversationBranchNodePath : [];
10725
+ var leaf = String(path[path.length - 1] || state.activeConversationBranchId || '');
10726
+ if (leaf) return rememberViewedBranchNodeId(owner, leaf);
10727
+ }
10728
+ if (!state.viewedBranchNodeIdsByTarget) return '';
10729
+ return String(state.viewedBranchNodeIdsByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)] || '');
10730
+ }
10731
+
10732
+ // 分支级缓存键:同一会话的两个分页绝不共用一份缓存。
10733
+ function branchConversationViewKey(target) {
10734
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
10735
+ if (!owner) return '';
10736
+ var branch = viewedBranchNodeIdFor(owner) || 'root';
10737
+ return runtimeKeyFor(owner.workspaceId, owner.conversationId) + '::branch::' + branch;
10738
+ }
10739
+
10657
10740
  function registerRuntimeKey(target, runtimeKey) {
10658
10741
  runtimeKey = String(runtimeKey || '');
10659
10742
  if (!target || !runtimeKey) return runtimeKeyFor(target && target.workspaceId, target && target.conversationId);
@@ -10811,6 +10894,11 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
10811
10894
  var messages = guideMessagesForTarget(target);
10812
10895
  var pending = Object.keys(messages).map(function(id) { return messages[id]; }).filter(function(item) {
10813
10896
  if (!item || persistedGuideIds[item.clientMessageId]) return false;
10897
+ // dev-0.6.4: Guide 只有被内核接收后才出现在时间线上,并且显示在接收位置。
10898
+ // 乐观/待确认(awaitingAck)与 deferred/rejected 的 Guide 都不进入对话区。
10899
+ if (item.awaitingAck) return false;
10900
+ var guideStatus = String(item.status || '').toLowerCase();
10901
+ if (['accepted', 'applied'].indexOf(guideStatus) < 0) return false;
10814
10902
  // A Guide already owned by its work run is rendered by the Build block
10815
10903
  // (expanded events or collapsed guide list), never as a separate trailing
10816
10904
  // message. Without this guard, collapse/expand rebuilds would re-add the
@@ -10829,26 +10917,6 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
10829
10917
  element.remove();
10830
10918
  element = null;
10831
10919
  }
10832
- if (item.awaitingAck && item.runId) {
10833
- var runElement = findWorkRunElement({ runId: item.runId });
10834
- var runBody = runElement && runElement.querySelector('.conversation-work-run-body');
10835
- if (runBody) {
10836
- var pendingStack = runBody.querySelector(':scope > .work-run-pending-guides');
10837
- if (!pendingStack) {
10838
- pendingStack = document.createElement('div');
10839
- pendingStack.className = 'work-run-pending-guides';
10840
- runBody.appendChild(pendingStack);
10841
- }
10842
- if (!element || !pendingStack.contains(element)) {
10843
- var holder = document.createElement('div');
10844
- holder.innerHTML = renderWorkRunGuideMessage(Object.assign({}, item, { status: 'accepted' }));
10845
- element = holder.firstElementChild;
10846
- if (element) pendingStack.appendChild(element);
10847
- }
10848
- if (element) applyGuideMessageMeta(element, item);
10849
- return;
10850
- }
10851
- }
10852
10920
  if (element) applyGuideMessageMeta(element, item);
10853
10921
  else addMsg('user', item.content || 'Guide', 'guide', item.model || state.model, undefined, {
10854
10922
  clientMessageId: item.clientMessageId,
@@ -10915,6 +10983,7 @@ function setConversationRuntimeState(target, status, runId, extra) {
10915
10983
  if (!state.runningConversations) state.runningConversations = {};
10916
10984
  if (['running', 'stopping', 'force_restarting'].indexOf(next.status) >= 0) state.runningConversations[key] = next;
10917
10985
  else delete state.runningConversations[key];
10986
+ if (typeof ensureRuntimeReconcileWatchdog === 'function') ensureRuntimeReconcileWatchdog();
10918
10987
  var branchIds = conversationBranchIdsForTarget(target);
10919
10988
  var runs = workRunsForBranch(target, branchIds.runtime || branchIds.viewed);
10920
10989
  var matchingRun = next.runId && runs.find(function(item) { return String(item.runId || '') === next.runId; });
@@ -11258,13 +11327,15 @@ function workRunBeforeElement(run) {
11258
11327
 
11259
11328
  function workRunsForTarget(target) {
11260
11329
  target = target || currentConversationTarget();
11261
- var key = runtimeKeyFor(target.workspaceId, target.conversationId);
11262
- if (!state.workRunsByTarget) state.workRunsByTarget = {};
11263
- if (!state.workRunsByTarget[key]) {
11330
+ // 读取「当前浏览的分支节点」自己的 Build 列表。`workRunsByTarget` 只是
11331
+ // 兼容性的展示镜像,按会话共享会让另一个分页的运行结果覆盖本页(新发送的
11332
+ // 消息看起来消失),因此渲染路径一律走按分支节点存储的账本。
11333
+ var viewedBranchId = viewedBranchNodeIdFor(target);
11334
+ if (!viewedBranchId) {
11264
11335
  var branchIds = conversationBranchIdsForTarget(target);
11265
- state.workRunsByTarget[key] = workRunsForBranch(target, branchIds.viewed);
11336
+ viewedBranchId = branchIds.viewed;
11266
11337
  }
11267
- return state.workRunsByTarget[key];
11338
+ return workRunsForBranch(target, viewedBranchId);
11268
11339
  }
11269
11340
 
11270
11341
  function syncWorkRunsSnapshot(runs, target, branchId) {
@@ -11642,9 +11713,12 @@ function renderWorkRunGuideMessage(event) {
11642
11713
  var guideId = String(guide.guideId || event.guideId || '');
11643
11714
  var messageIndex = Number(state.guideMessageIndexByClientId && state.guideMessageIndexByClientId[clientMessageId]);
11644
11715
  var indexed = Number.isFinite(messageIndex) && messageIndex >= 0;
11716
+ // dev-0.6.4: 时间线里的 Guide 记录不再提供编辑入口。Guide 是当前 Build 的
11717
+ // 历史干预记录,改写它既不会重放 Build,也会让分支归属变得含糊;这里只保留
11718
+ // 复制动作,消息索引属性仍然写回,供锚点/诊断使用。
11645
11719
  return '<div class="chat-msg user work-run-guide-message" data-client-message-id="' + escAttr(clientMessageId) + '" data-guide-id="' + escAttr(guideId) + '" data-run-id="' + escAttr(String(guide.runId || event.runId || '')) + '"' + (indexed ? ' data-message-index="' + messageIndex + '"' : '') + '>' +
11646
11720
  '<div class="meta"><span class="msg-role">' + esc(t('message.user')) + '</span>' + (time ? '<span> | ' + esc(time) + '</span>' : '') +
11647
- '<span class="guide-receipt-status ' + escAttr(status) + '">Guide · ' + esc(guideUiStatusLabel(status)) + '</span>' + messageActionsHtml('user', content, indexed ? messageIndex : -1) + '</div>' +
11721
+ '<span class="guide-receipt-status ' + escAttr(status) + '">Guide · ' + esc(guideUiStatusLabel(status)) + '</span>' + messageActionsHtml('user', content, -1) + '</div>' +
11648
11722
  '<div class="msg-body">' + renderMessageContent(content) + '</div></div>';
11649
11723
  }
11650
11724
 
@@ -11929,7 +12003,10 @@ function updateConversationWorkRunElement(run, element) {
11929
12003
  // 重建/增量更新前后保持聊天视窗滚动位置:贴底时跟随最新,未贴底时
11930
12004
  // 精确恢复 scrollTop,避免内容高度变化把视窗“强制拖向最新”。
11931
12005
  var chatEl = els['chat-area'];
11932
- 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);
11933
12010
  var scrollTopBefore = chatEl ? chatEl.scrollTop : 0;
11934
12011
  var activityOpenStates = {};
11935
12012
  var priorActivities = element.querySelectorAll ? element.querySelectorAll('details.conversation-work-activity[data-activity-key], details[data-work-detail-key]') : [];
@@ -12173,6 +12250,12 @@ function applyAgentWorkEventToRun(event) {
12173
12250
  if (run.userToggled !== true) run.expanded = run.status === 'error';
12174
12251
  }
12175
12252
  }
12253
+ // dev-0.6.4 hotfix: 排队出队的回合结束时,把权威转录(助手回复、最终用户行)
12254
+ // 刷进对话区。正常发送路径有自己的回执刷新;这里只针对后台出队的运行,
12255
+ // 且由 scheduleDrainedTurnTranscriptRefresh 做去抖与“无发送在飞”保护。
12256
+ if (['done', 'error', 'interrupted', 'force_interrupted'].indexOf(type) >= 0) {
12257
+ scheduleDrainedTurnTranscriptRefresh(target, []);
12258
+ }
12176
12259
  if (isActiveConversationTarget(target) && (!branchIds.viewed || !eventBranchId || branchIds.viewed === eventBranchId)) {
12177
12260
  state.workRunsByTarget[runtimeKeyFor(target.workspaceId, target.conversationId)] = runs;
12178
12261
  var existingElement = findWorkRunElement(run);
@@ -12455,7 +12538,8 @@ function renderPersistedToolMessage(message) {
12455
12538
 
12456
12539
  function conversationMessageCache(target) {
12457
12540
  var scopedTarget = target || currentConversationTarget();
12458
- var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
12541
+ // 分支级缓存:分页切换时另一个页面的消息快照绝不能覆盖当前页。
12542
+ var key = branchConversationViewKey(scopedTarget);
12459
12543
  if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
12460
12544
  return Array.isArray(state.conversationMessagesByTarget[key])
12461
12545
  ? state.conversationMessagesByTarget[key].map(function(message) { return Object.assign({}, message); })
@@ -12464,10 +12548,22 @@ function conversationMessageCache(target) {
12464
12548
 
12465
12549
  function cacheConversationMessages(messages, target) {
12466
12550
  var scopedTarget = target || currentConversationTarget();
12467
- var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
12551
+ var key = branchConversationViewKey(scopedTarget);
12468
12552
  var cloned = Array.isArray(messages) ? messages.map(function(message) { return Object.assign({}, message); }) : [];
12469
12553
  if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
12554
+ // Disk remains authoritative. Limit retained branch snapshots, not history.
12555
+ delete state.conversationMessagesByTarget[key];
12470
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
+ }
12471
12567
  return cloned.map(function(message) { return Object.assign({}, message); });
12472
12568
  }
12473
12569
 
@@ -12558,6 +12654,9 @@ function renderChatMessagesUnsafe(messages, target) {
12558
12654
  var chatWasAtBottom = shouldAutoScroll(els['chat-area']);
12559
12655
  els['chat-area'].innerHTML = '';
12560
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);
12561
12660
  var renderConversationId = renderTarget.conversationId;
12562
12661
  var guideMessages = guideMessagesForTarget(renderTarget);
12563
12662
  var persistedGuideIds = {};
@@ -12574,6 +12673,10 @@ function renderChatMessagesUnsafe(messages, target) {
12574
12673
  });
12575
12674
  var persistedRunsById = {};
12576
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 = {};
12577
12680
  var renderedRunIds = {};
12578
12681
  var recoveredAssistantRunIds = {};
12579
12682
  var messageRunRoles = {};
@@ -12583,6 +12686,7 @@ function renderChatMessagesUnsafe(messages, target) {
12583
12686
  if (runId) {
12584
12687
  persistedRunsById[runId] = run;
12585
12688
  persistedRunIndexes[runId] = runIndex;
12689
+ persistedRunIdSet[runId] = true;
12586
12690
  }
12587
12691
  });
12588
12692
  normalizedMessages.forEach(function(message, messageIndex) {
@@ -12643,7 +12747,7 @@ function renderChatMessagesUnsafe(messages, target) {
12643
12747
  finishToolBatch(renderConversationId);
12644
12748
  var clientMessageId = String(m.clientMessageId || '');
12645
12749
  if (clientMessageId) persistedGuideIds[clientMessageId] = true;
12646
- if (clientMessageId && m.runId && persistedRuns.some(function(run) { return String(run.runId || '') === String(m.runId || ''); })) {
12750
+ if (clientMessageId && m.runId && persistedRunIdSet[String(m.runId || '')]) {
12647
12751
  delete guideMessages[clientMessageId];
12648
12752
  continue;
12649
12753
  }
@@ -12662,7 +12766,9 @@ function renderChatMessagesUnsafe(messages, target) {
12662
12766
  guideId: m.guideId || '',
12663
12767
  attachments: m.attachments || [],
12664
12768
  target: renderTarget,
12665
- timestamp: m.timestamp || ''
12769
+ timestamp: m.timestamp || '',
12770
+ skipAutoScroll: true,
12771
+ skipEntryAnimation: true
12666
12772
  });
12667
12773
  if (associatedRun && String(m.role || '') === 'user') renderOwnedWorkRun(associatedRun, false);
12668
12774
  if (clientMessageId) delete guideMessages[clientMessageId];
@@ -12675,7 +12781,6 @@ function renderChatMessagesUnsafe(messages, target) {
12675
12781
  });
12676
12782
  if (pendingWorkReview) addWorkReview(pendingWorkReview.diffs || pendingWorkReview, pendingWorkReview.runId || '');
12677
12783
  renderConversationBranchPagers();
12678
- renderAutoRouteRatingControls();
12679
12784
  if (typeof renderPendingOptionsInChat === 'function') renderPendingOptionsInChat(state.pendingOptions || []);
12680
12785
  }
12681
12786
 
@@ -12684,6 +12789,153 @@ function renderChatMessagesUnsafe(messages, target) {
12684
12789
  // the preceding window via conversation:loadEarlier and prepends it, so a
12685
12790
  // long-running conversation can be browsed without ever transferring the whole
12686
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
+
12687
12939
  function renderLoadEarlierButton() {
12688
12940
  if (!els['chat-area']) return;
12689
12941
  Array.prototype.forEach.call(els['chat-area'].querySelectorAll('.conversation-load-earlier'), function(button) { button.remove(); });
@@ -12714,9 +12966,12 @@ function renderLoadEarlierButton() {
12714
12966
  var mergedMessages = result.chatMessages.concat(newer);
12715
12967
  state.conversationLoadedBefore[loadKey] = Math.max(0, Number(result.windowStart) || 0);
12716
12968
  var chatWasAtBottom = shouldAutoScroll(els['chat-area']);
12717
- renderChatMessages(mergedMessages, loadTarget);
12718
- if (!chatWasAtBottom && els['chat-area']) els['chat-area'].scrollTop = 0;
12719
- 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
+ }
12720
12975
  }).catch(function() { button.disabled = false; });
12721
12976
  });
12722
12977
  els['chat-area'].insertBefore(button, els['chat-area'].firstChild);
@@ -12745,9 +13000,20 @@ function renderConversationBranchPagers() {
12745
13000
  if (matched) anchor = candidate;
12746
13001
  return matched;
12747
13002
  });
12748
- if (!anchor && !sourceMessageId && !sourceGuideId) {
13003
+ if (!anchor) {
13004
+ // dev-0.6.4: 分页锚点回退。Build 开头的用户消息可能以内联 Build/Guide
13005
+ // 卡片渲染,没有写回 data-message-id;旧逻辑此时直接放弃,导致「从 build
13006
+ // 开头编辑」创建的分页看不到分页条。现在按渲染索引,再按锚点文本回退。
12749
13007
  var sourceMessageIndex = Math.max(0, Number(branchGroup.sourceMessageIndex === undefined ? activeBranch.sourceMessageIndex : branchGroup.sourceMessageIndex));
12750
13008
  anchor = els['chat-area'].querySelector('.chat-msg.user[data-message-index="' + sourceMessageIndex + '"]');
13009
+ if (!anchor && String(activeBranch.sourceText || '').trim()) {
13010
+ var wantedAnchorText = String(activeBranch.sourceText || '').trim();
13011
+ Array.prototype.some.call(els['chat-area'].querySelectorAll('.chat-msg.user'), function(candidate) {
13012
+ var candidateText = String(candidate && candidate._newmarkMessageText || '').trim();
13013
+ if (candidateText && candidateText === wantedAnchorText) { anchor = candidate; return true; }
13014
+ return false;
13015
+ });
13016
+ }
12751
13017
  }
12752
13018
  if (anchor) {
12753
13019
  var pager = document.createElement('div');
@@ -12852,6 +13118,8 @@ function scheduleStreamingWorkflowTextFlush(ui, eventConversationId, eventWorksp
12852
13118
  requestAnimationFrame(function() {
12853
13119
  var current = conversationWorkUiState(eventConversationId, eventWorkspaceId);
12854
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;
12855
13123
  applyStreamingWorkflowText(current, eventConversationId, current._streamMode || state.mode, current._streamModel || state.model);
12856
13124
  });
12857
13125
  }
@@ -13288,6 +13556,9 @@ function hydrateConversationBranchState(snapshot) {
13288
13556
  if (Object.prototype.hasOwnProperty.call(s, 'branchGroupId')) state.activeConversationBranchGroupId = String(s.branchGroupId || '');
13289
13557
  if (Object.prototype.hasOwnProperty.call(s, 'viewedBranchNodePath')) state.viewedConversationBranchNodePath = Array.isArray(s.viewedBranchNodePath) ? s.viewedBranchNodePath.map(String) : [];
13290
13558
  if (Object.prototype.hasOwnProperty.call(s, 'runtimeBranchNodePath')) state.runtimeConversationBranchNodePath = Array.isArray(s.runtimeBranchNodePath) ? s.runtimeBranchNodePath.map(String) : [];
13559
+ // 分支级缓存必须知道「当前浏览的是哪个节点」,否则换页后另一页的快照会覆盖本页。
13560
+ var viewedLeaf = String((state.viewedConversationBranchNodePath || []).slice(-1)[0] || state.activeConversationBranchId || '');
13561
+ rememberViewedBranchNodeId(currentConversationTarget(s.conversationId || activeConversationId()), viewedLeaf);
13291
13562
  rememberRuntimeConversationBranchPath(currentConversationTarget(s.conversationId || activeConversationId()));
13292
13563
  }
13293
13564
 
@@ -13320,6 +13591,15 @@ function applyConversationModelSelection(snapshot) {
13320
13591
  }
13321
13592
  }
13322
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
+
13323
13603
  function applyConversationSnapshot(s, requestedConversationId, options) {
13324
13604
  if (!s) return;
13325
13605
  if (s.computerUse) {
@@ -13339,23 +13619,34 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13339
13619
  var preserveReadableHistory = !!(options && options.preserveReadableHistory
13340
13620
  && !snapshotHasReadableConversationHistory(s)
13341
13621
  && (!options.allowEmptyHistory || conversationHasReadableHistory(snapshotTarget)));
13622
+ // dev-0.6.6 长程内存回收:宿主可能只回传"转录未变"的瘦快照(仅当本渲染端此前
13623
+ // 通过 ackTranscript 声明过同一修订号)。瘦快照只对"当前正画着这条对话"有效;
13624
+ // 其它情况必须取一次全量,绝不能用别的对话的画面顶替。
13625
+ var transcriptUnchanged = !!(s && s.transcriptUnchanged === true);
13342
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
+ }
13343
13635
  hydrateConversationDraftSnapshot(s, snapshotTarget);
13344
- hydrateConversationBranchState(s);
13636
+ if (!transcriptUnchanged) hydrateConversationBranchState(s);
13345
13637
  var modelRevisionMatches = !options || options.modelRevision === undefined
13346
13638
  || Number(state.modelSelectionRevision || 0) === Number(options.modelRevision || 0);
13347
13639
  if (modelRevisionMatches && isActiveConversationTarget(snapshotTarget)) {
13348
13640
  applyConversationModelSelection(s);
13349
13641
  }
13350
13642
  rebindQueueToRuntimeBranch(snapshotTarget);
13351
- if (s && Array.isArray(s.workRuns) && !preserveReadableHistory) {
13643
+ if (s && Array.isArray(s.workRuns) && !preserveReadableHistory && !transcriptUnchanged) {
13352
13644
  var viewedBranchIdForSync = Array.isArray(s.viewedBranchNodePath) && s.viewedBranchNodePath.length
13353
13645
  ? String(s.viewedBranchNodePath[s.viewedBranchNodePath.length - 1])
13354
13646
  : '';
13355
13647
  syncWorkRunsSnapshot(s.workRuns, snapshotTarget, viewedBranchIdForSync || undefined);
13356
13648
  }
13357
- applyAutoRouteRatingState(s);
13358
- if (s && Number.isFinite(Number(s.totalMessages))) {
13649
+ if (s && Number.isFinite(Number(s.totalMessages)) && Array.isArray(s.chatMessages)) {
13359
13650
  if (!state.conversationLoadedBefore) state.conversationLoadedBefore = {};
13360
13651
  var windowKey = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
13361
13652
  state.conversationLoadedBefore[windowKey] = Math.max(0, Number(s.totalMessages) - (Array.isArray(s.chatMessages) ? s.chatMessages.length : 0));
@@ -13367,9 +13658,16 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13367
13658
  window.renderContextWindow();
13368
13659
  }
13369
13660
  if (state.rightTab === 'status') window.renderRightStatusPanel();
13370
- if (s && Array.isArray(s.chatMessages) && !preserveReadableHistory) {
13661
+ if (s && Array.isArray(s.chatMessages) && !preserveReadableHistory && !transcriptUnchanged) {
13371
13662
  renderChatMessages(s.chatMessages, snapshotTarget);
13372
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
+ }
13373
13671
  }
13374
13672
  if (s && s.conversationId) {
13375
13673
  state.activeBackendConversationId = s.conversationId;
@@ -13381,6 +13679,17 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13381
13679
  if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, snapshotTarget);
13382
13680
  }
13383
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
+ }
13384
13693
  if (s && Array.isArray(s.workEvents)) {
13385
13694
  var eventConversationId = runtimeKeyFor(snapshotTarget.workspaceId, s.conversationId || requestedConversationId || 'default');
13386
13695
  if (!state.agentWorkEventsByConversation) state.agentWorkEventsByConversation = {};
@@ -13400,13 +13709,29 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13400
13709
  if (s && s.subagents) state.subagents = s.subagents;
13401
13710
  if (s && s.linkedPlan) state.linkedPlan = s.linkedPlan;
13402
13711
  replayActiveAgentWorkEvents();
13403
- // 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;
13404
13723
  var runs = state.workRunsByTarget && state.workRunsByTarget[runtimeKeyFor(snapshotTarget.workspaceId, snapshotTarget.conversationId)] || [];
13405
13724
  for (var ri = 0; ri < runs.length; ri++) {
13406
- if (runs[ri].status === 'running') {
13725
+ if (runs[ri].status !== 'running') continue;
13726
+ if (backendStillRunning) {
13407
13727
  setConversationRuntimeState(snapshotTarget, 'running', runs[ri].runId);
13728
+ continue;
13408
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]);
13409
13733
  }
13734
+ if (typeof reconcileLocalRuntimeFromSnapshot === 'function') reconcileLocalRuntimeFromSnapshot(s, snapshotTarget);
13410
13735
  var isRunning = !!runningConversationRecord(snapshotTarget.conversationId, snapshotTarget.workspaceId);
13411
13736
  setWorking(isRunning);
13412
13737
  }
@@ -13449,10 +13774,54 @@ function optionQuestionKey(question, options, index) {
13449
13774
  return JSON.stringify({ index: index, question: question || '', options: (options || []).map(optionLabel) });
13450
13775
  }
13451
13776
 
13452
- function addOptionBlock(question, options, questionKey) {
13777
+ // dev-0.6.4 hotfix: 选项块携带归属会话,作答必须回到提问的那个会话。
13778
+ function optionBlockOwner(element) {
13779
+ var block = element && element.closest ? element.closest('[data-option-owner-conversation]') : null;
13780
+ if (!block) return currentConversationTarget();
13781
+ return {
13782
+ workspaceId: String(block.getAttribute('data-option-owner-workspace') || '') || runtimeWorkspaceId(''),
13783
+ conversationId: String(block.getAttribute('data-option-owner-conversation') || '') || activeConversationId()
13784
+ };
13785
+ }
13786
+
13787
+ function pendingOptionsForTarget(target) {
13788
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
13789
+ if (!owner) return [];
13790
+ if (!state.pendingOptionsByTarget) state.pendingOptionsByTarget = {};
13791
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
13792
+ return Array.isArray(state.pendingOptionsByTarget[key]) ? state.pendingOptionsByTarget[key] : [];
13793
+ }
13794
+
13795
+ function setPendingOptionsForTarget(options, target) {
13796
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
13797
+ if (!owner) return;
13798
+ if (!state.pendingOptionsByTarget) state.pendingOptionsByTarget = {};
13799
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
13800
+ state.pendingOptionsByTarget[key] = (Array.isArray(options) ? options : []).map(function(question) {
13801
+ return {
13802
+ id: String(question && question.id || ''),
13803
+ question: String(question && question.question || ''),
13804
+ options: (Array.isArray(question && question.options) ? question.options : []).map(function(option) {
13805
+ return typeof option === 'string' ? option : { label: String(option && option.label || ''), description: String(option && option.description || '') };
13806
+ })
13807
+ };
13808
+ });
13809
+ if (isActiveConversationTarget(owner)) state.pendingOptions = state.pendingOptionsByTarget[key];
13810
+ }
13811
+
13812
+ function addOptionBlock(question, options, questionKey, ownerAttributes) {
13453
13813
  var div = document.createElement('div');
13454
13814
  div.className = 'chat-msg assistant';
13455
13815
  div.setAttribute('data-option-question', encodeURIComponent(questionKey));
13816
+ if (ownerAttributes) {
13817
+ var ownerHolder = document.createElement('div');
13818
+ ownerHolder.innerHTML = '<span' + ownerAttributes + '></span>';
13819
+ var ownerNode = ownerHolder.firstElementChild;
13820
+ if (ownerNode) {
13821
+ div.setAttribute('data-option-owner-workspace', ownerNode.getAttribute('data-option-owner-workspace') || '');
13822
+ div.setAttribute('data-option-owner-conversation', ownerNode.getAttribute('data-option-owner-conversation') || '');
13823
+ }
13824
+ }
13456
13825
  var html = '<div class="option-block"><div class="option-question">' + esc(question) + '</div>';
13457
13826
  for (var i = 0; i < options.length; i++) {
13458
13827
  var label = optionLabel(options[i]);
@@ -13471,11 +13840,18 @@ function addOptionBlock(question, options, questionKey) {
13471
13840
  }
13472
13841
 
13473
13842
  function renderPendingOptionsInChat(options) {
13843
+ // dev-0.6.4 hotfix: 选项提问按会话归属。旧实现把全局 pendingOptions 渲染到当前
13844
+ // 对话区,后台会话的提问会“溢出”到前台对话,点击后还会用当前会话发送答案,
13845
+ // 等于把提问激活给了另一个对话。
13846
+ if (options === undefined) options = pendingOptionsForTarget(currentConversationTarget());
13474
13847
  options = options || [];
13475
13848
  if (!options.length) return;
13849
+ var owner = currentConversationTarget();
13850
+ var ownerAttributes = ' data-option-owner-workspace="' + escAttr(String(owner.workspaceId || '')) + '" data-option-owner-conversation="' + escAttr(String(owner.conversationId || 'default')) + '"';
13476
13851
  var batchKey = JSON.stringify(options.map(function(q, index) { return optionQuestionKey(q.question || q.id || '', q.options || [], index); }));
13477
- if (state.pendingOptionBatchKey !== batchKey) {
13478
- state.pendingOptionBatchKey = batchKey;
13852
+ var batchKeyWithOwner = String(owner.workspaceId || '') + '::' + String(owner.conversationId || 'default') + '::' + batchKey;
13853
+ if (state.pendingOptionBatchKey !== batchKeyWithOwner) {
13854
+ state.pendingOptionBatchKey = batchKeyWithOwner;
13479
13855
  state.pendingOptionAnswers = {};
13480
13856
  }
13481
13857
  for (var i = 0; i < options.length; i++) {
@@ -13485,7 +13861,7 @@ function renderPendingOptionsInChat(options) {
13485
13861
  var encodedKey = encodeURIComponent(key);
13486
13862
  if (els['chat-area'] && els['chat-area'].querySelector('[data-option-question="' + encodedKey + '"]')) continue;
13487
13863
  state.renderedOptionKeys[key] = true;
13488
- addOptionBlock(q.question || q.id || t('status.question'), q.options || [], key);
13864
+ addOptionBlock(q.question || q.id || t('status.question'), q.options || [], key, ownerAttributes);
13489
13865
  }
13490
13866
  }
13491
13867
 
@@ -13544,6 +13920,15 @@ function handlePlanExecutionChoice(questionKey, opt) {
13544
13920
 
13545
13921
  window.optionSelected = function(questionKey, opt, button) {
13546
13922
  if (!questionKey || state.pendingOptionAnswers[questionKey] !== undefined) return;
13923
+ // dev-0.6.4 hotfix: 作答必须回到提问的那个会话;后台会话的提问不允许借用
13924
+ // 当前对话的输入框发送(旧实现用 window.sendMessage() 直接发到当前会话)。
13925
+ var answerOwner = optionBlockOwner(button);
13926
+ if (!isActiveConversationTarget(answerOwner)) {
13927
+ showUiNotice(currentLang() === 'zh'
13928
+ ? '该提问属于另一个对话,请先切换到那个对话再作答。'
13929
+ : 'That question belongs to another conversation; switch to it before answering.', 'error', 'option-owner-mismatch');
13930
+ return;
13931
+ }
13547
13932
  if (state.mode === 'plan' && handlePlanExecutionChoice(questionKey, opt)) {
13548
13933
  var planBlock = button && button.closest ? button.closest('[data-option-question]') : null;
13549
13934
  if (planBlock) Array.from(planBlock.querySelectorAll('.option-btn')).forEach(function(candidate) {
@@ -13560,7 +13945,7 @@ window.optionSelected = function(questionKey, opt, button) {
13560
13945
  candidate.classList.toggle('selected', candidate === button);
13561
13946
  });
13562
13947
  }
13563
- var questions = state.pendingOptions || [];
13948
+ var questions = pendingOptionsForTarget(answerOwner);
13564
13949
  var answered = questions.map(function(q, index) {
13565
13950
  var key = optionQuestionKey(q.question || q.id || '', q.options || [], index);
13566
13951
  return { question: q.question || q.id || t('status.question'), answer: state.pendingOptionAnswers[key] };
@@ -13581,6 +13966,13 @@ window.optionSelected = function(questionKey, opt, button) {
13581
13966
  };
13582
13967
 
13583
13968
  window.optionManualSubmitted = function(element) {
13969
+ var manualOwner = optionBlockOwner(element);
13970
+ if (!isActiveConversationTarget(manualOwner)) {
13971
+ showUiNotice(currentLang() === 'zh'
13972
+ ? '该提问属于另一个对话,请先切换到那个对话再作答。'
13973
+ : 'That question belongs to another conversation; switch to it before answering.', 'error', 'option-owner-mismatch');
13974
+ return;
13975
+ }
13584
13976
  var manual = element && element.closest ? element.closest('.option-manual') : null;
13585
13977
  if (!manual) return;
13586
13978
  var input = manual.querySelector('.option-manual-input');
@@ -13774,6 +14166,66 @@ async function reconcileStaleLocalRuntimeBeforeSend(target) {
13774
14166
  return true;
13775
14167
  }
13776
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
+
13777
14229
  function setSubmitButtonVisual(icon, label, running, stopMode) {
13778
14230
  var btn = els['submit-btn'] || document.getElementById('submit-btn');
13779
14231
  if (!btn) return;
@@ -14457,19 +14909,14 @@ function modelDeploymentValueForName(name, providerId) {
14457
14909
  var modelName = String(name || '').trim();
14458
14910
  if (!modelName) return '';
14459
14911
  var providers = Array.isArray(state.providers) ? state.providers : [];
14460
- var match = null;
14461
- for (var pi = 0; pi < providers.length && !match; pi++) {
14462
- var provider = providers[pi];
14463
- if (providerId && String(provider.id || provider.name || '') !== String(providerId)) continue;
14464
- var models = Array.isArray(provider.models) ? provider.models : [];
14465
- for (var mi = 0; mi < models.length && !match; mi++) {
14466
- var entryName = typeof models[mi] === 'string' ? models[mi] : (models[mi] && models[mi].name || '');
14467
- if (entryName === modelName) match = { provider: provider, name: entryName };
14468
- }
14469
- }
14470
- if (!match) return modelName;
14471
- var providerIdValue = String(match.provider.id || match.provider.name || '');
14472
- 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);
14473
14920
  }
14474
14921
 
14475
14922
  // 不可用模型回退同步输入框下方选择区:固定模型选择被回退时,选择区
@@ -17273,13 +17720,24 @@ function queueIndexesForTarget(target) {
17273
17720
  var requests = state.nextQueueRequests || [];
17274
17721
  for (var index = 0; index < queue.length; index++) {
17275
17722
  if (!queuedRequestMatchesTarget(requests[index], target)) continue;
17276
- var requestBranchPath = String(requests[index] && requests[index].branchPath || '');
17277
- var runtimeBranchPath = queueBranchPathForTarget(target, 'runtime');
17278
- if (!requestBranchPath || !runtimeBranchPath || requestBranchPath === runtimeBranchPath) indexes.push(index);
17723
+ // dev-0.6.4: 排队行永远可见。它属于「受理时捕获的分支」,但它是这个会话里
17724
+ // 真实存在的用户输入;旧逻辑按当前运行分支过滤,分页之后整队行被隐形,
17725
+ // 用户既看不到内容也无法恢复。可见性与行绑定的分支解耦(分支只决定它
17726
+ // 何时、在哪一页执行)。
17727
+ indexes.push(index);
17279
17728
  }
17280
17729
  return indexes;
17281
17730
  }
17282
17731
 
17732
+ // 行绑定的分支与当前运行分支不一致时,该行会等待(内核会暂停而不是改投)。
17733
+ // 这个状态必须在界面上可见,用户才能切回对应分页再恢复队列。
17734
+ function queueRequestBranchMismatch(target, request) {
17735
+ var requestBranchPath = String(request && request.branchPath || '');
17736
+ var runtimeBranchPath = queueBranchPathForTarget(target, 'runtime');
17737
+ if (!requestBranchPath || !runtimeBranchPath) return false;
17738
+ return requestBranchPath !== runtimeBranchPath;
17739
+ }
17740
+
17283
17741
  function queueIndexesForRuntimeTarget(target) {
17284
17742
  var indexes = [];
17285
17743
  var queue = state.nextQueue || [];
@@ -17312,7 +17770,9 @@ function rebindQueueToRuntimeBranch(target) {
17312
17770
  if (!runtimePath || !key) return;
17313
17771
  (state.nextQueueRequests || []).forEach(function(request) {
17314
17772
  if (!request || !queuedRequestMatchesTarget(request, target)) return;
17315
- request.branchPath = runtimePath;
17773
+ // dev-0.6.4: 只刷新会话 runtime 身份,绝不改写受理时捕获的分支路径。
17774
+ // 静默改绑会把排队输入从用户提交的分页挪到另一条分支,正是
17775
+ // 「跨分支复用/串分支」必须禁止的行为。
17316
17776
  request.runtimeKey = key;
17317
17777
  });
17318
17778
  }
@@ -17377,6 +17837,21 @@ function setQueueItemsForTarget(items, target) {
17377
17837
  runId: String(item && item.runId || ''),
17378
17838
  createdAt: String(item && item.createdAt || ''),
17379
17839
  images: (Array.isArray(item && item.images) ? item.images : []).map(function(image) { return Object.assign({}, image); }),
17840
+ // dev-0.6.4: 保留受理时捕获的分支身份。旧实现只保留展示字段,排队行在
17841
+ // 重新投影时会被默认绑到「当前运行分支」,等于静默改绑到另一条分页,
17842
+ // 也让「等待所属分页」的状态永远无法显示。
17843
+ branchNodeId: String(item && item.branchNodeId || ''),
17844
+ branchPath: Array.isArray(item && item.branchPath)
17845
+ ? item.branchPath.map(function(value) { return String(value || ''); }).filter(Boolean)
17846
+ : (item && item.branchPath ? [String(item.branchPath)] : []),
17847
+ ledgerBranchId: String(item && item.ledgerBranchId || ''),
17848
+ targetRuntimeKey: String(item && item.targetRuntimeKey || ''),
17849
+ workspaceKey: String(item && item.workspaceKey || ''),
17850
+ modelSelection: String(item && item.modelSelection || ''),
17851
+ // dev-0.6.4 hotfix: 保留账本给出的受阻原因,界面才能显示“被失败前置阻断”
17852
+ // 并提供显式的修复入口。
17853
+ waitingReason: String(item && item.waitingReason || ''),
17854
+ blocked: item && item.blocked === true,
17380
17855
  };
17381
17856
  });
17382
17857
  }
@@ -17412,7 +17887,23 @@ function applyConversationCommandSnapshot(snapshot, target) {
17412
17887
  if (!state.queuePausedByTarget) state.queuePausedByTarget = {};
17413
17888
  state.queuePausedByTarget[key] = snapshot.queuePaused;
17414
17889
  }
17415
- if (Array.isArray(snapshot.queueItems)) setQueueItemsForTarget(snapshot.queueItems, target);
17890
+ if (Array.isArray(snapshot.queueItems)) {
17891
+ // dev-0.6.4 hotfix: 排队行真正进入对话(出队)时,后端会把这一轮写成正式用户
17892
+ // 消息,但渲染端不会收到发送回执,因此此前对话区一直不显示这条用户输入。
17893
+ // 这里比较权威队列的条数变化,检测到出队就刷新一次转录。
17894
+ var previousQueueItems = queueItemsForTarget(target);
17895
+ var previousQueueIds = previousQueueItems.map(function(item) { return String(item && item.id || ''); }).filter(Boolean);
17896
+ var nextQueueIds = snapshot.queueItems.map(function(item) { return String(item && item.id || ''); }).filter(Boolean);
17897
+ var drainedIds = previousQueueIds.filter(function(id) { return nextQueueIds.indexOf(id) < 0; });
17898
+ setQueueItemsForTarget(snapshot.queueItems, target);
17899
+ if (drainedIds.length) scheduleDrainedTurnTranscriptRefresh(target, drainedIds);
17900
+ }
17901
+ if (Array.isArray(snapshot.pendingOptions)) setPendingOptionsForTarget(snapshot.pendingOptions, target);
17902
+ // dev-0.6.4: goal 停止/清除后,任何携带 goal 字段的权威快照都要同步前端显示。
17903
+ if (Object.prototype.hasOwnProperty.call(snapshot, 'goal') && isActiveConversationTarget(target)) {
17904
+ applyReturnedGoalState(snapshot);
17905
+ if (window.renderInputStack) window.renderInputStack();
17906
+ }
17416
17907
  var queue = snapshot.queued && typeof snapshot.queued === 'object' ? snapshot.queued : snapshot.queue;
17417
17908
  if (queue && typeof queue === 'object') {
17418
17909
  setBackendQueueForTarget(queue, target);
@@ -17449,6 +17940,55 @@ function applyConversationCommandSnapshot(snapshot, target) {
17449
17940
  }
17450
17941
  }
17451
17942
 
17943
+ /**
17944
+ * dev-0.6.4 hotfix: 排队输入出队进入对话后,把权威转录刷新到对话区。
17945
+ *
17946
+ * 走发送路径的回合有本地乐观气泡 + 回执刷新;排队回合由内核在后台启动,渲染端
17947
+ * 既没有乐观气泡也没有回执,于是用户输入在对话区“消失”。这里在检测到排队行
17948
+ * 出队时按目标做一次去抖刷新:只在当前前台会话、且没有渲染端发送在飞时执行,
17949
+ * 避免与发送回执互相覆盖。
17950
+ */
17951
+ function scheduleDrainedTurnTranscriptRefresh(target, drainedIds) {
17952
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
17953
+ if (!owner || !api.getState) return;
17954
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
17955
+ if (!isActiveConversationTarget(owner)) return;
17956
+ if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[key]) return;
17957
+ if (!state.pendingDrainRefreshByTarget) state.pendingDrainRefreshByTarget = {};
17958
+ if (state.pendingDrainRefreshByTarget[key]) return;
17959
+ // 出队事件早于正式用户消息落盘,因此按几个时间点各刷新一次:首次拿到用户输入,
17960
+ // 后续拿到助手回复;全部完成后清除登记,下一次出队可再次触发。
17961
+ state.pendingDrainRefreshByTarget[key] = { ids: (drainedIds || []).slice(), timers: [] };
17962
+ var pending = state.pendingDrainRefreshByTarget[key];
17963
+ [150, 1200, 3000].forEach(function(delay, index, all) {
17964
+ pending.timers.push(setTimeout(function() {
17965
+ refreshConversationTranscriptFromBackend(owner);
17966
+ if (index === all.length - 1 && state.pendingDrainRefreshByTarget) delete state.pendingDrainRefreshByTarget[key];
17967
+ }, delay));
17968
+ });
17969
+ }
17970
+
17971
+ function refreshConversationTranscriptFromBackend(target) {
17972
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
17973
+ if (!owner || !api.getState || !isActiveConversationTarget(owner)) return Promise.resolve(null);
17974
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
17975
+ if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[key]) return Promise.resolve(null);
17976
+ return Promise.resolve(api.getState(owner)).then(function(snapshot) {
17977
+ if (!snapshot || typeof snapshot !== 'object') return null;
17978
+ if (!isActiveConversationTarget(owner)) return null;
17979
+ if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[key]) return null;
17980
+ // dev-0.6.4: goal 完成/停止后,前端必须在收到权威快照时同步清除 goal 显示。
17981
+ applyReturnedGoalState(snapshot);
17982
+ if (Array.isArray(snapshot.workRuns)) syncWorkRunsSnapshot(snapshot.workRuns, owner);
17983
+ if (Array.isArray(snapshot.chatMessages)) {
17984
+ hydrateConversationBranchState(snapshot);
17985
+ cacheConversationMessages(snapshot.chatMessages, owner);
17986
+ renderChatMessages(snapshot.chatMessages, owner);
17987
+ }
17988
+ return snapshot;
17989
+ }).catch(function() { return null; });
17990
+ }
17991
+
17452
17992
  window.queueAction = function(action, input, target) {
17453
17993
  if (!api.queueAction) return Promise.resolve({ ok: false, error: 'queueAction unavailable' });
17454
17994
  var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
@@ -17549,6 +18089,15 @@ window.renderQueuePanel = function() {
17549
18089
  var visibleIndexes = queueIndexesForTarget(target);
17550
18090
  var count = visibleIndexes.length;
17551
18091
  panel.style.display = count ? '' : 'none';
18092
+ // dev-0.6.4: 新排队输入必须被看见。旧行为把队列面板默认折叠,配合按分支
18093
+ // 过滤的隐形逻辑,用户提交的排队内容在界面上完全不出现。条数增加时自动
18094
+ // 展开,用户手动折叠在条数不变时仍然有效。
18095
+ if (!state.queueSeenCountsByTarget) state.queueSeenCountsByTarget = {};
18096
+ // 仅用于「新增排队行时自动展开面板」的本地记账键,不参与执行身份判定。
18097
+ var seenKey = String(target.workspaceId || '') + '::' + String(target.conversationId || 'default');
18098
+ var seenCount = Number(state.queueSeenCountsByTarget[seenKey] || 0);
18099
+ if (count > seenCount) state.queueCollapsed = false;
18100
+ state.queueSeenCountsByTarget[seenKey] = count;
17552
18101
  panel.classList.toggle('collapsed', !!state.queueCollapsed);
17553
18102
  label.textContent = t('queue.title') + ' ' + count;
17554
18103
  var pauseToggle = document.getElementById('queue-pause-btn');
@@ -17592,17 +18141,73 @@ window.renderQueuePanel = function() {
17592
18141
  '<button type="button" class="stack-icon-btn queue-guide-btn" onclick="window.guideQueueItem(' + eventIndex + ')" title="' + escAttr(t('queue.guideNow')) + '">' + iconSvg('corner-down-right', t('queue.guideNow'), 'tiny') + '<span>' + esc(t('queue.guideAction')) + '</span></button>' +
17593
18142
  '<button class="stack-icon-btn" onclick="window.focusQueueItem(' + eventIndex + ')" title="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</button>' +
17594
18143
  '<button class="stack-icon-btn danger" onclick="window.deleteQueueItem(' + eventIndex + ')" title="' + escAttr(t('common.delete')) + '">' + iconSvg('trash', t('common.delete'), 'tiny') + '</button>' : '';
17595
- html += '<div class="queue-item" data-queue-index="' + queueIndex + '"' + identityAttributes + dragAttributes + '>' +
18144
+ // 绑定了另一条分页的行会等待而不是改投,这个状态必须在界面上显式可见,
18145
+ // 否则用户会以为排队输入被丢弃。
18146
+ var waitingForBranch = queueRequestBranchMismatch(target, request);
18147
+ var waitingLabel = currentLang() === 'zh'
18148
+ ? '等待它所属的分页:切回该分页后队列可恢复执行'
18149
+ : 'Waiting for its own page: switch back to that page to resume the queue';
18150
+ // dev-0.6.4 hotfix: 被失败前置阻断的排队行必须显式可见,否则用户只会看到
18151
+ // “恢复后什么都不发生”。恢复队列时内核会把这些行重新挂到最后一个已提交 Build。
18152
+ var authoritativeItem = queueItemId
18153
+ ? queueItemsForTarget(target).find(function(item) { return item.id === queueItemId; })
18154
+ : null;
18155
+ var blockedRow = !!(authoritativeItem && (authoritativeItem.blocked === true || authoritativeItem.waitingReason));
18156
+ var blockedLabel = currentLang() === 'zh'
18157
+ ? '这一行被失败的前置 Build 阻断:按“恢复队列”会把它重新挂到最后一个已提交 Build 后执行'
18158
+ : 'Blocked by a failed predecessor: resuming the queue re-anchors this row to the last committed build';
18159
+ html += '<div class="queue-item" data-queue-index="' + queueIndex + '"' + identityAttributes
18160
+ + (waitingForBranch ? ' data-queue-branch-waiting="1" title="' + escAttr(waitingLabel) + '"' : '')
18161
+ + (blockedRow ? ' data-queue-blocked="1" title="' + escAttr(blockedLabel) + '"' : '')
18162
+ + dragAttributes + '>' +
17596
18163
  dragHandle +
17597
18164
  '<input class="queue-edit" value="' + escAttr(state.nextQueue[queueIndex] || '') + '"' + inputAttributes + '>' +
17598
18165
  '<span class="stack-actions">' + actions + '</span>' +
17599
18166
  '</div>';
17600
18167
  }
17601
18168
  list.innerHTML = html;
18169
+
18170
+ // dev-0.6.4 hotfix: 存在受阻行时提供显式修复入口。0.6.3 合约要求失败前置继续
18171
+ // 阻断后继(不静默跳过),因此修复必须是用户主动动作,并且可见、可审计。
18172
+ var blockedCount = 0;
18173
+ for (var blockedIndex = 0; blockedIndex < visibleIndexes.length; blockedIndex++) {
18174
+ var blockedRequest = state.nextQueueRequests[visibleIndexes[blockedIndex]];
18175
+ var blockedItemId = blockedRequest ? String(blockedRequest.queueItemId || '') : '';
18176
+ var blockedItem = blockedItemId
18177
+ ? queueItemsForTarget(target).find(function(item) { return item.id === blockedItemId; })
18178
+ : null;
18179
+ if (blockedItem && (blockedItem.blocked === true || blockedItem.waitingReason)) blockedCount++;
18180
+ }
18181
+ var headerActions = panel.querySelector('#queue-header .stack-actions');
18182
+ var repairButton = document.getElementById('queue-repair-btn');
18183
+ if (blockedCount) {
18184
+ if (!repairButton && headerActions) {
18185
+ repairButton = document.createElement('button');
18186
+ repairButton.className = 'stack-icon-btn';
18187
+ repairButton.id = 'queue-repair-btn';
18188
+ repairButton.setAttribute('type', 'button');
18189
+ repairButton.setAttribute('onclick', 'event.stopPropagation();window.repairBlockedQueue()');
18190
+ var pauseButton = document.getElementById('queue-pause-btn');
18191
+ if (pauseButton && pauseButton.parentNode === headerActions) headerActions.insertBefore(repairButton, pauseButton);
18192
+ else headerActions.appendChild(repairButton);
18193
+ }
18194
+ if (repairButton) {
18195
+ var repairLabel = currentLang() === 'zh' ? '修复受阻队列' : 'Repair blocked queue';
18196
+ repairButton.title = repairLabel;
18197
+ repairButton.innerHTML = iconSvg('wrench', repairLabel, 'tiny') + '<span class="icon-only-label">' + esc(repairLabel) + '</span>';
18198
+ }
18199
+ } else if (repairButton && repairButton.parentNode) {
18200
+ repairButton.remove();
18201
+ }
17602
18202
  };
17603
18203
 
17604
18204
  window.renderNextQueue = window.renderQueuePanel;
17605
18205
 
18206
+ // 显式修复受阻队列:把被失败前置阻断的排队行重新挂到最后一个已提交 Build。
18207
+ window.repairBlockedQueue = function() {
18208
+ return window.queueAction('repair_blocked', {}, currentConversationTarget());
18209
+ };
18210
+
17606
18211
  window.toggleQueuePanel = function() {
17607
18212
  state.queueCollapsed = !state.queueCollapsed;
17608
18213
  window.renderInputStack();
@@ -18203,8 +18808,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18203
18808
  addMsg('user', displayText, opts.fromQueue ? 'next-build' : requestedMode, state.model, opts.optimisticMessageIndex, { attachments: optimisticAttachments });
18204
18809
  }
18205
18810
  renderConversationBranchPagers();
18206
- applyAutoRouteRatingState(null);
18207
- renderAutoRouteRatingControls();
18208
18811
  }
18209
18812
  var responseMsg = null;
18210
18813
  var sendFailure = '';
@@ -18248,7 +18851,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18248
18851
  setConversationRuntimeState(lockedTarget, receivedSendStatus, r.runId, { provisional: false, generation: r.generation || 0, runtimeKey: r.runtimeKey || '' });
18249
18852
  }
18250
18853
  var stillActive = sendResultOwnsRuntime && sendLoadGeneration === state.conversationLoadGeneration && isActiveConversationTarget(lockedTarget) && isViewingRuntimeConversationBranch(lockedTarget);
18251
- if (stillActive) applyAutoRouteRatingState(r);
18252
18854
  if (stillActive && r && Array.isArray(r.workRuns)) syncWorkRunsSnapshot(r.workRuns, lockedTarget);
18253
18855
  var responseOwnedByRun = !!(r && Array.isArray(r.workRuns) && r.workRuns.some(function(run) {
18254
18856
  return (run.events || []).some(function(event) { return ['response', 'final_response'].indexOf(String(event.type || '').toLowerCase()) >= 0; });
@@ -18351,7 +18953,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18351
18953
  }
18352
18954
  if (stillActiveAfterRefresh && s && s.conversationPlan) state.conversationPlan = normalizeConversationPlan(s.conversationPlan);
18353
18955
  if (stillActiveAfterRefresh && s && Array.isArray(s.workRuns)) {
18354
- applyAutoRouteRatingState(s);
18355
18956
  hydrateConversationBranchState(s);
18356
18957
  syncWorkRunsSnapshot(s.workRuns, lockedTarget);
18357
18958
  if (Array.isArray(s.chatMessages)) renderChatMessages(s.chatMessages, lockedTarget);
@@ -18944,8 +19545,6 @@ window.refreshRightStatus = function(options) {
18944
19545
  var statusContextRequest = captureContextWindowSnapshotRequest(statusTarget);
18945
19546
  return api.getState(statusTarget).then(function(s) {
18946
19547
  if (!isActiveConversationTarget(statusTarget) || statusLoadGeneration !== state.conversationLoadGeneration) return null;
18947
- applyAutoRouteRatingState(s);
18948
- renderAutoRouteRatingControls();
18949
19548
  applyConversationCommandSnapshot(s, statusTarget);
18950
19549
  if (s && s.model !== undefined) state.model = s.model;
18951
19550
  if (s && s.modelLabel) state.modelLabel = s.modelLabel;
@@ -18962,7 +19561,7 @@ window.refreshRightStatus = function(options) {
18962
19561
  }
18963
19562
  if (s && s.subagents) state.subagents = s.subagents;
18964
19563
  if (s && s.fileDiffs) state.fileDiffs = s.fileDiffs;
18965
- if (s && s.pendingOptions) state.pendingOptions = s.pendingOptions;
19564
+ if (s && s.pendingOptions) setPendingOptionsForTarget(s.pendingOptions, computerTarget || currentConversationTarget(s.conversationId || requestedConversationId || activeConversationId()));
18966
19565
  // Bind the Flow takeover to backend per-conversation state. The takeover
18967
19566
  // for the requested target reflects exactly what the backend holds for that
18968
19567
  // conversation: a suspension (paused) or an active running Flow. It is
@@ -19848,7 +20447,9 @@ function applyReturnedGoalState(snapshot) {
19848
20447
  if (!snapshot || !Object.prototype.hasOwnProperty.call(snapshot, 'goal')) return;
19849
20448
  state.goalText = snapshot.goal && snapshot.goal.objective ? String(snapshot.goal.objective) : '';
19850
20449
  state.goalVisible = !!state.goalText;
19851
- state.goalPaused = !!(snapshot.goal && snapshot.goal.paused);
20450
+ // dev-0.6.4: goal 结束后端会停止/清空 goal,但前端此前只清 text/visible,paused
20451
+ // 残留会让 goal 条继续以“暂停”状态显示。这里在无 goal 时一并清除暂停态。
20452
+ state.goalPaused = state.goalVisible ? !!(snapshot.goal && snapshot.goal.paused) : false;
19852
20453
  if (!snapshot.goal && snapshot.mode === 'build') {
19853
20454
  state.mode = 'build';
19854
20455
  if (els['mode-select']) {
@@ -20217,6 +20818,29 @@ function renderGeneralSettings() {
20217
20818
  '</div>';
20218
20819
  }
20219
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
+
20220
20844
  function renderModelSettings() {
20221
20845
  var providersHtml = '';
20222
20846
  for (var i = 0; i < state.providers.length; i++) {
@@ -20292,11 +20916,11 @@ function renderModelSettings() {
20292
20916
  '<option value="off"' + (state.fallbackOnUnavailable === 'off' ? ' selected' : '') + '>' + esc(t('common.off')) + '</option>' +
20293
20917
  '<option value="on"' + (state.fallbackOnUnavailable === 'on' ? ' selected' : '') + '>' + esc(t('common.on')) + '</option></select></div></div>' +
20294
20918
  '<div class="setting-row"><span class="setting-label">' + esc(t('model.preference')) + '</span><div class="setting-control"><select onchange="window.setSwitchTendency(this.value)">' +
20295
- '<option value="quality"' + (state.switchTendency === 'quality' ? ' selected' : '') + '>' + esc(t('model.qualityPolicy')) + '</option>' +
20296
- '<option value="balanced"' + (state.switchTendency === 'balanced' ? ' selected' : '') + '>' + esc(t('model.balancedPolicy')) + '</option>' +
20297
- '<option value="cost"' + (state.switchTendency === 'cost' ? ' selected' : '') + '>' + esc(t('model.costPolicy')) + '</option>' +
20298
- '<option value="speed"' + (state.switchTendency === 'speed' ? ' selected' : '') + '>' + esc(t('model.speedPolicy')) + '</option></select></div></div>' +
20299
- '<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() +
20300
20924
  '</div>' +
20301
20925
  '<div class="provider-card" style="margin-top:12px;">' +
20302
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>' +
@@ -20777,6 +21401,47 @@ window.setAutoSwitchMode = function(v) {
20777
21401
  state.autoSwitchScope = v === 'provider' ? 'provider' : 'all';
20778
21402
  api.saveConfig({ autoSwitch: state.autoSwitch, autoSwitchScope: state.autoSwitchScope });
20779
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
+ });
20780
21445
  };
20781
21446
 
20782
21447
  var systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)');
@@ -20919,14 +21584,10 @@ window.setFallbackOnUnavailable = function(v) {
20919
21584
  };
20920
21585
 
20921
21586
  window.setSwitchTendency = function(v) {
20922
- state.switchTendency = ['quality', 'balanced', 'cost', 'speed'].indexOf(v) >= 0 ? v : 'balanced';
21587
+ state.switchTendency = ['conservative', 'balanced', 'aggressive'].indexOf(v) >= 0 ? v : 'balanced';
20923
21588
  api.saveConfig({ switchTendency: state.switchTendency });
20924
21589
  };
20925
21590
 
20926
- window.clearLearnedAutoPreferences = function() {
20927
- api.saveConfig({ clearLearnedAutoPreferences: true }).catch(function(){});
20928
- };
20929
-
20930
21591
  window.setNativeToolEnabled = function(name, enabled) {
20931
21592
  if (!name) return;
20932
21593
  state.nativeToolEnabled = state.nativeToolEnabled || {};
@@ -21037,6 +21698,7 @@ window.removeProvider = function(idx) {
21037
21698
 
21038
21699
  window.addModel = function() {
21039
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>' +
21040
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>' +
21041
21703
  '<div class="auto-input-group"><label>' + esc(t('model.contextSize')) + '</label><input id="new-model-ctx" type="number" value="128000" placeholder="tokens"></div>' +
21042
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>' +
@@ -21068,7 +21730,8 @@ window.serializeThinkingTierMap = function(map) {
21068
21730
  };
21069
21731
 
21070
21732
  window.saveNewModel = function() {
21071
- 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();
21072
21735
  var provIdx = parseInt(document.getElementById('new-model-provider').value);
21073
21736
  var ctxEl = document.getElementById('new-model-ctx');
21074
21737
  var visionEl = document.getElementById('new-model-vision');
@@ -21077,10 +21740,11 @@ window.saveNewModel = function() {
21077
21740
  var tierMapEl = document.getElementById('new-model-tier-map');
21078
21741
  if (name && !isNaN(provIdx) && state.providers[provIdx]) {
21079
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; }
21080
21744
  var tierMap = window.parseThinkingTierMap(tierMapEl ? tierMapEl.value : '');
21081
21745
  var entry = {
21082
21746
  name: name,
21083
- display: name,
21747
+ display: display || name,
21084
21748
  description: descEl ? descEl.value : '',
21085
21749
  max_tokens: ctxEl ? parseInt(ctxEl.value || '128000') : 128000,
21086
21750
  vision: !!(visionEl && visionEl.checked),
@@ -21111,8 +21775,10 @@ window.editModel = function(provIdx, modelIdx) {
21111
21775
  var model = provider.models[modelIdx];
21112
21776
  if (!model) return;
21113
21777
  if (typeof model === 'string') model = { name: model, display: model };
21778
+ state.modelEditIdentity = { providerId: provider.id, modelName: model.name };
21114
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>' +
21115
- '<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>' +
21116
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>' +
21117
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>' +
21118
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>' +
@@ -21123,7 +21789,11 @@ window.editModel = function(provIdx, modelIdx) {
21123
21789
  };
21124
21790
 
21125
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; });
21126
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;
21127
21797
  if (!oldProvider || !oldProvider.models || !oldProvider.models[modelIdx]) return;
21128
21798
  var nameEl = document.getElementById('edit-model-name');
21129
21799
  var provEl = document.getElementById('edit-model-provider');
@@ -21133,15 +21803,17 @@ window.saveModelEdit = function(oldProvIdx, modelIdx) {
21133
21803
  var descEl = document.getElementById('edit-model-desc');
21134
21804
  var tierMapEl = document.getElementById('edit-model-tier-map');
21135
21805
  var name = nameEl ? nameEl.value.trim() : '';
21136
- var newProvIdx = provEl ? parseInt(provEl.value) : oldProvIdx;
21806
+ var newProvIdx = oldProvIdx;
21807
+ var displayEl = document.getElementById('edit-model-display');
21137
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; }
21138
21810
  var previous = oldProvider.models[modelIdx];
21139
21811
  if (typeof previous === 'string') previous = { name: previous, display: previous };
21140
21812
  var tierMap = window.parseThinkingTierMap(tierMapEl ? tierMapEl.value : '');
21141
21813
  var updated = Object.assign({}, previous, {
21142
21814
  _previous_name: previous.name || name,
21143
21815
  name: name,
21144
- display: name,
21816
+ display: displayEl ? displayEl.value.trim() || name : name,
21145
21817
  description: descEl ? descEl.value : '',
21146
21818
  max_tokens: ctxEl ? parseInt(ctxEl.value || '4096') : 4096,
21147
21819
  vision: !!(visionEl && visionEl.checked),
@@ -24312,6 +24984,34 @@ window.activateConversationWithGlass = function(idx, row) {
24312
24984
  } });
24313
24985
  };
24314
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
+
24315
25015
  function renderConversations() {
24316
25016
  var list = els['conversation-list'];
24317
25017
  if (!list) return;
@@ -24472,7 +25172,8 @@ function renderConversations() {
24472
25172
  var stale = existingRows[id];
24473
25173
  if (stale && stale.parentNode === list) stale.parentNode.removeChild(stale);
24474
25174
  });
24475
- orderedRows.forEach(function(row, idx) {
25175
+ var rowsToPlace = conversationRowOrderDuringDrag(orderedRows, list);
25176
+ rowsToPlace.forEach(function(row, idx) {
24476
25177
  var current = list.children[idx];
24477
25178
  if (current === row) return;
24478
25179
  list.insertBefore(row, current || null);
@@ -24561,6 +25262,7 @@ window.newConversation = function(workspaceReference, branchCommunication) {
24561
25262
  state._promptBoundKey = conversationDraftKey();
24562
25263
  renderConversations();
24563
25264
  els['chat-area'].innerHTML = '';
25265
+ state.renderedChatTargetKey = '';
24564
25266
  var target = currentConversationTarget(id);
24565
25267
  var modelRevisionAtActivation = Number(state.modelSelectionRevision || 0);
24566
25268
  var activation = api.activateConversation ? api.activateConversation(target) : (api.ensureConversation ? api.ensureConversation(target) : Promise.resolve(null));
@@ -24648,6 +25350,11 @@ window.switchConversation = function(idx) {
24648
25350
  setActiveWorkspaceConversation(idx);
24649
25351
  var activeId = activeConversationId();
24650
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);
24651
25358
  var activeBrowserView = browserGuestForTarget(activeBrowserTarget);
24652
25359
  if (activeBrowserView) setActiveBrowserGuest(activeBrowserView, activeBrowserTarget);
24653
25360
  else if (state.rightTab === 'browser') window.ensureBrowserPanel({ target: activeBrowserTarget, activate: true }).catch(function(error) {
@@ -24774,6 +25481,7 @@ window.archiveConv = function(conversationId) {
24774
25481
  if (priorActiveId === targetId) {
24775
25482
  state.activeBackendConversationId = '';
24776
25483
  if (els['chat-area']) els['chat-area'].innerHTML = '';
25484
+ state.renderedChatTargetKey = '';
24777
25485
  if (window.renderFlowTakeover) window.renderFlowTakeover(false, '', { target: targetRuntime });
24778
25486
  }
24779
25487
  } else {
@@ -24786,6 +25494,7 @@ window.archiveConv = function(conversationId) {
24786
25494
  if (priorActiveId === targetId) {
24787
25495
  state.activeBackendConversationId = nextActiveId;
24788
25496
  if (els['chat-area']) els['chat-area'].innerHTML = '';
25497
+ state.renderedChatTargetKey = '';
24789
25498
  }
24790
25499
  }
24791
25500
  if (state.flowTakeovers) {
@@ -26665,7 +27374,15 @@ window.commitConversationDomOrder = function() {
26665
27374
  var ids = Array.from(list.querySelectorAll('.conv-item')).map(function(row) {
26666
27375
  return String(row.getAttribute('data-conversation-id') || '');
26667
27376
  }).filter(function(id) { return !!byId[id]; });
26668
- 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);
26669
27386
  var activeId = activeConversationId();
26670
27387
  convs.splice.apply(convs, [0, convs.length].concat(ids.map(function(id) { return byId[id]; })));
26671
27388
  setActiveWorkspaceConversationById(activeId);
@@ -28140,7 +28857,6 @@ function schedulePostStartupUiRendering() {
28140
28857
  if (Array.isArray(s.wslDistros)) state.wslDistros = s.wslDistros;
28141
28858
  if (s.conversationId) state.activeBackendConversationId = s.conversationId;
28142
28859
  if (s.conversationPlan) state.conversationPlan = normalizeConversationPlan(s.conversationPlan);
28143
- applyAutoRouteRatingState(s);
28144
28860
  if (s.chatMessages) {
28145
28861
  hydrateConversationBranchState(s);
28146
28862
  renderChatMessages(s.chatMessages);
@@ -28148,10 +28864,11 @@ function schedulePostStartupUiRendering() {
28148
28864
  if (s.defaultFlow) state.defaultFlow = s.defaultFlow;
28149
28865
  if (s.autoSwitch !== undefined) state.autoSwitch = s.autoSwitch ? 'on' : 'off';
28150
28866
  if (s.autoSwitchScope) state.autoSwitchScope = s.autoSwitchScope === 'provider' ? 'provider' : 'all';
28867
+ window.applyJevDecisionModelState(s);
28151
28868
  if (s.openAIApiMode) state.openAIApiMode = (s.openAIApiMode === 'chat' || s.openAIApiMode === 'responses') ? s.openAIApiMode : 'chat_stream';
28152
28869
  if (s.fallbackOnUnavailable !== undefined) state.fallbackOnUnavailable = s.fallbackOnUnavailable ? 'on' : 'off';
28153
28870
  if (s.switchTendency) {
28154
- 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';
28155
28872
  }
28156
28873
  if (s.providers) {
28157
28874
  state.providers = s.providers;