newmark-agent 0.6.2 → 0.6.4

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.
@@ -3373,6 +3373,19 @@ button.todo-item { background: transparent; }
3373
3373
  .queue-item.dragging { opacity: 0.58; transform: scale(0.99); }
3374
3374
  .queue-item.queue-drag-displaced { transition: transform 120ms var(--ease-out-expo); }
3375
3375
 
3376
+ /* dev-0.6.4: 该排队行绑定的是另一条分页,正在等待自己的运行分支。 */
3377
+ .queue-item[data-queue-branch-waiting="1"] { opacity: 0.68; }
3378
+ .queue-item[data-queue-branch-waiting="1"] .queue-edit { font-style: italic; }
3379
+
3380
+ /* dev-0.6.4: 该排队行被失败的前置 Build 阻断,恢复队列会重新挂载到已提交 frontier。 */
3381
+ .queue-item[data-queue-blocked="1"] .queue-edit { color: var(--danger, #e06c75); }
3382
+ .queue-item[data-queue-blocked="1"]::after {
3383
+ content: '!';
3384
+ color: var(--danger, #e06c75);
3385
+ font: 10px var(--font-mono);
3386
+ }
3387
+
3388
+
3376
3389
  .queue-drag {
3377
3390
  width: 22px;
3378
3391
  height: 22px;
@@ -7143,6 +7156,10 @@ var state = {
7143
7156
  _conversationLoadingMsgEl: null,
7144
7157
  _syncedMode: null,
7145
7158
  _syncedModel: null,
7159
+ // Monotonic revision for explicit renderer model changes. Activation
7160
+ // snapshots may restore the conversation's remembered model only when no
7161
+ // newer local selection was made while the snapshot was in flight.
7162
+ modelSelectionRevision: 0,
7146
7163
  _syncedInputMode: null,
7147
7164
  _editingQueueIndex: -1,
7148
7165
  _editingGoalText: '',
@@ -7207,6 +7224,8 @@ var state = {
7207
7224
  pendingOptions: [],
7208
7225
  pendingOptionAnswers: {},
7209
7226
  pendingOptionBatchKey: '',
7227
+ pendingOptionsByTarget: {},
7228
+ pendingOptionAnswersByTarget: {},
7210
7229
  promptAttachments: [],
7211
7230
  nextQueue: [],
7212
7231
  nextQueueRequests: [],
@@ -7217,6 +7236,7 @@ var state = {
7217
7236
  backendQueue: { steering: [], followUp: [] },
7218
7237
  backendQueuesByTarget: {},
7219
7238
  queueItemsByTarget: {},
7239
+ pendingDrainRefreshByTarget: {},
7220
7240
  defaultFlow: '',
7221
7241
  settingsActiveTab: 'general',
7222
7242
  globalAgentPrompt: '',
@@ -9353,7 +9373,7 @@ var api = window.api || {
9353
9373
  conversationId: target.conversationId,
9354
9374
  action: action
9355
9375
  };
9356
- ['id', 'text', 'value', 'requestedMode', 'goalObjective', 'createdAt', 'images', 'paused', 'orderedIds'].forEach(function(field) {
9376
+ ['id', 'text', 'value', 'requestedMode', 'goalObjective', 'createdAt', 'images', 'paused', 'orderedIds', 'branchNodeId', 'branchPath'].forEach(function(field) {
9357
9377
  if (input && Object.prototype.hasOwnProperty.call(input, field)) body[field] = input[field];
9358
9378
  });
9359
9379
  var r = await fetch('/api/queue-action', {
@@ -9950,8 +9970,12 @@ window.submitUserMessageEdit = async function(message, messageIndex, editedText,
9950
9970
  if (!isActiveConversationTarget(target)) return;
9951
9971
  clearAgentWorkEvents(conversationId);
9952
9972
  resetConversationWorkUi(conversationId, false);
9953
- state.backendQueue = setBackendQueueForTarget({ steering: [], followUp: [] }, target);
9954
- if (window.syncNextQueueFromBackend) window.syncNextQueueFromBackend(state.backendQueue, target);
9973
+ // dev-0.6.4: 生成分页时绝不按新分支的清空队列覆盖本地投影。排队行是用户
9974
+ // 已经提交的输入,属于它受理时捕获的那条分支;旧逻辑在这里把队列整体
9975
+ // 清零,于是分页之后排队内容从界面上消失。内核的执行投影仍然只跟随当前
9976
+ // 激活分支(新分支绝不接管另一页的排队输入),刷新由切回该分页时的
9977
+ // 权威快照完成;这里只保留可见性并标记「等待所属分页」。
9978
+ if (result && Array.isArray(result.queueItems)) setQueueItemsForTarget(result.queueItems, target);
9955
9979
  state.conversationBranches = Array.isArray(result.branches) ? result.branches : [];
9956
9980
  state.conversationBranchGroups = Array.isArray(result.branchGroups) ? result.branchGroups : [];
9957
9981
  state.activeConversationBranchId = String(result.activeBranchId || '');
@@ -10650,6 +10674,47 @@ function runtimeKeyFor(workspaceId, conversationId) {
10650
10674
  return state.runtimeKeyAliases && state.runtimeKeyAliases[base] || base;
10651
10675
  }
10652
10676
 
10677
+ // dev-0.6.4: 全对话 id(分支身份)。用户分页编辑产生的分支与实验性「分支交流」
10678
+ // 产生的分支共用这一条规则:`<conversationId>::branch:<branchNodeId>`。
10679
+ // 供应商会话序列 id 与所有分支级缓存都必须使用它;兄弟分支绝不共享消息、
10680
+ // Build 列表或折叠历史。
10681
+ function conversationBranchIdentity(conversationId, branchNodeId) {
10682
+ var conversation = String(conversationId || 'default') || 'default';
10683
+ var branch = String(branchNodeId || '');
10684
+ return branch ? conversation + '::branch:' + branch : conversation;
10685
+ }
10686
+
10687
+ // 记录每个会话最近一次「被浏览」的分支节点,供后台会话的缓存键使用。
10688
+ function rememberViewedBranchNodeId(target, branchNodeId) {
10689
+ var owner = normalizeQueuedConversationTarget(target);
10690
+ if (!owner) return '';
10691
+ if (!state.viewedBranchNodeIdsByTarget) state.viewedBranchNodeIdsByTarget = {};
10692
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
10693
+ var branch = String(branchNodeId || '');
10694
+ if (branch) state.viewedBranchNodeIdsByTarget[key] = branch;
10695
+ return String(state.viewedBranchNodeIdsByTarget[key] || '');
10696
+ }
10697
+
10698
+ function viewedBranchNodeIdFor(target) {
10699
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
10700
+ if (!owner) return '';
10701
+ if (isActiveConversationTarget(owner)) {
10702
+ var path = Array.isArray(state.viewedConversationBranchNodePath) ? state.viewedConversationBranchNodePath : [];
10703
+ var leaf = String(path[path.length - 1] || state.activeConversationBranchId || '');
10704
+ if (leaf) return rememberViewedBranchNodeId(owner, leaf);
10705
+ }
10706
+ if (!state.viewedBranchNodeIdsByTarget) return '';
10707
+ return String(state.viewedBranchNodeIdsByTarget[runtimeKeyFor(owner.workspaceId, owner.conversationId)] || '');
10708
+ }
10709
+
10710
+ // 分支级缓存键:同一会话的两个分页绝不共用一份缓存。
10711
+ function branchConversationViewKey(target) {
10712
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
10713
+ if (!owner) return '';
10714
+ var branch = viewedBranchNodeIdFor(owner) || 'root';
10715
+ return runtimeKeyFor(owner.workspaceId, owner.conversationId) + '::branch::' + branch;
10716
+ }
10717
+
10653
10718
  function registerRuntimeKey(target, runtimeKey) {
10654
10719
  runtimeKey = String(runtimeKey || '');
10655
10720
  if (!target || !runtimeKey) return runtimeKeyFor(target && target.workspaceId, target && target.conversationId);
@@ -10807,6 +10872,11 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
10807
10872
  var messages = guideMessagesForTarget(target);
10808
10873
  var pending = Object.keys(messages).map(function(id) { return messages[id]; }).filter(function(item) {
10809
10874
  if (!item || persistedGuideIds[item.clientMessageId]) return false;
10875
+ // dev-0.6.4: Guide 只有被内核接收后才出现在时间线上,并且显示在接收位置。
10876
+ // 乐观/待确认(awaitingAck)与 deferred/rejected 的 Guide 都不进入对话区。
10877
+ if (item.awaitingAck) return false;
10878
+ var guideStatus = String(item.status || '').toLowerCase();
10879
+ if (['accepted', 'applied'].indexOf(guideStatus) < 0) return false;
10810
10880
  // A Guide already owned by its work run is rendered by the Build block
10811
10881
  // (expanded events or collapsed guide list), never as a separate trailing
10812
10882
  // message. Without this guard, collapse/expand rebuilds would re-add the
@@ -10825,26 +10895,6 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
10825
10895
  element.remove();
10826
10896
  element = null;
10827
10897
  }
10828
- if (item.awaitingAck && item.runId) {
10829
- var runElement = findWorkRunElement({ runId: item.runId });
10830
- var runBody = runElement && runElement.querySelector('.conversation-work-run-body');
10831
- if (runBody) {
10832
- var pendingStack = runBody.querySelector(':scope > .work-run-pending-guides');
10833
- if (!pendingStack) {
10834
- pendingStack = document.createElement('div');
10835
- pendingStack.className = 'work-run-pending-guides';
10836
- runBody.appendChild(pendingStack);
10837
- }
10838
- if (!element || !pendingStack.contains(element)) {
10839
- var holder = document.createElement('div');
10840
- holder.innerHTML = renderWorkRunGuideMessage(Object.assign({}, item, { status: 'accepted' }));
10841
- element = holder.firstElementChild;
10842
- if (element) pendingStack.appendChild(element);
10843
- }
10844
- if (element) applyGuideMessageMeta(element, item);
10845
- return;
10846
- }
10847
- }
10848
10898
  if (element) applyGuideMessageMeta(element, item);
10849
10899
  else addMsg('user', item.content || 'Guide', 'guide', item.model || state.model, undefined, {
10850
10900
  clientMessageId: item.clientMessageId,
@@ -11254,13 +11304,15 @@ function workRunBeforeElement(run) {
11254
11304
 
11255
11305
  function workRunsForTarget(target) {
11256
11306
  target = target || currentConversationTarget();
11257
- var key = runtimeKeyFor(target.workspaceId, target.conversationId);
11258
- if (!state.workRunsByTarget) state.workRunsByTarget = {};
11259
- if (!state.workRunsByTarget[key]) {
11307
+ // 读取「当前浏览的分支节点」自己的 Build 列表。`workRunsByTarget` 只是
11308
+ // 兼容性的展示镜像,按会话共享会让另一个分页的运行结果覆盖本页(新发送的
11309
+ // 消息看起来消失),因此渲染路径一律走按分支节点存储的账本。
11310
+ var viewedBranchId = viewedBranchNodeIdFor(target);
11311
+ if (!viewedBranchId) {
11260
11312
  var branchIds = conversationBranchIdsForTarget(target);
11261
- state.workRunsByTarget[key] = workRunsForBranch(target, branchIds.viewed);
11313
+ viewedBranchId = branchIds.viewed;
11262
11314
  }
11263
- return state.workRunsByTarget[key];
11315
+ return workRunsForBranch(target, viewedBranchId);
11264
11316
  }
11265
11317
 
11266
11318
  function syncWorkRunsSnapshot(runs, target, branchId) {
@@ -11638,9 +11690,12 @@ function renderWorkRunGuideMessage(event) {
11638
11690
  var guideId = String(guide.guideId || event.guideId || '');
11639
11691
  var messageIndex = Number(state.guideMessageIndexByClientId && state.guideMessageIndexByClientId[clientMessageId]);
11640
11692
  var indexed = Number.isFinite(messageIndex) && messageIndex >= 0;
11693
+ // dev-0.6.4: 时间线里的 Guide 记录不再提供编辑入口。Guide 是当前 Build 的
11694
+ // 历史干预记录,改写它既不会重放 Build,也会让分支归属变得含糊;这里只保留
11695
+ // 复制动作,消息索引属性仍然写回,供锚点/诊断使用。
11641
11696
  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 + '"' : '') + '>' +
11642
11697
  '<div class="meta"><span class="msg-role">' + esc(t('message.user')) + '</span>' + (time ? '<span> | ' + esc(time) + '</span>' : '') +
11643
- '<span class="guide-receipt-status ' + escAttr(status) + '">Guide · ' + esc(guideUiStatusLabel(status)) + '</span>' + messageActionsHtml('user', content, indexed ? messageIndex : -1) + '</div>' +
11698
+ '<span class="guide-receipt-status ' + escAttr(status) + '">Guide · ' + esc(guideUiStatusLabel(status)) + '</span>' + messageActionsHtml('user', content, -1) + '</div>' +
11644
11699
  '<div class="msg-body">' + renderMessageContent(content) + '</div></div>';
11645
11700
  }
11646
11701
 
@@ -12169,6 +12224,12 @@ function applyAgentWorkEventToRun(event) {
12169
12224
  if (run.userToggled !== true) run.expanded = run.status === 'error';
12170
12225
  }
12171
12226
  }
12227
+ // dev-0.6.4 hotfix: 排队出队的回合结束时,把权威转录(助手回复、最终用户行)
12228
+ // 刷进对话区。正常发送路径有自己的回执刷新;这里只针对后台出队的运行,
12229
+ // 且由 scheduleDrainedTurnTranscriptRefresh 做去抖与“无发送在飞”保护。
12230
+ if (['done', 'error', 'interrupted', 'force_interrupted'].indexOf(type) >= 0) {
12231
+ scheduleDrainedTurnTranscriptRefresh(target, []);
12232
+ }
12172
12233
  if (isActiveConversationTarget(target) && (!branchIds.viewed || !eventBranchId || branchIds.viewed === eventBranchId)) {
12173
12234
  state.workRunsByTarget[runtimeKeyFor(target.workspaceId, target.conversationId)] = runs;
12174
12235
  var existingElement = findWorkRunElement(run);
@@ -12451,7 +12512,8 @@ function renderPersistedToolMessage(message) {
12451
12512
 
12452
12513
  function conversationMessageCache(target) {
12453
12514
  var scopedTarget = target || currentConversationTarget();
12454
- var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
12515
+ // 分支级缓存:分页切换时另一个页面的消息快照绝不能覆盖当前页。
12516
+ var key = branchConversationViewKey(scopedTarget);
12455
12517
  if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
12456
12518
  return Array.isArray(state.conversationMessagesByTarget[key])
12457
12519
  ? state.conversationMessagesByTarget[key].map(function(message) { return Object.assign({}, message); })
@@ -12460,7 +12522,7 @@ function conversationMessageCache(target) {
12460
12522
 
12461
12523
  function cacheConversationMessages(messages, target) {
12462
12524
  var scopedTarget = target || currentConversationTarget();
12463
- var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
12525
+ var key = branchConversationViewKey(scopedTarget);
12464
12526
  var cloned = Array.isArray(messages) ? messages.map(function(message) { return Object.assign({}, message); }) : [];
12465
12527
  if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
12466
12528
  state.conversationMessagesByTarget[key] = cloned;
@@ -12741,9 +12803,20 @@ function renderConversationBranchPagers() {
12741
12803
  if (matched) anchor = candidate;
12742
12804
  return matched;
12743
12805
  });
12744
- if (!anchor && !sourceMessageId && !sourceGuideId) {
12806
+ if (!anchor) {
12807
+ // dev-0.6.4: 分页锚点回退。Build 开头的用户消息可能以内联 Build/Guide
12808
+ // 卡片渲染,没有写回 data-message-id;旧逻辑此时直接放弃,导致「从 build
12809
+ // 开头编辑」创建的分页看不到分页条。现在按渲染索引,再按锚点文本回退。
12745
12810
  var sourceMessageIndex = Math.max(0, Number(branchGroup.sourceMessageIndex === undefined ? activeBranch.sourceMessageIndex : branchGroup.sourceMessageIndex));
12746
12811
  anchor = els['chat-area'].querySelector('.chat-msg.user[data-message-index="' + sourceMessageIndex + '"]');
12812
+ if (!anchor && String(activeBranch.sourceText || '').trim()) {
12813
+ var wantedAnchorText = String(activeBranch.sourceText || '').trim();
12814
+ Array.prototype.some.call(els['chat-area'].querySelectorAll('.chat-msg.user'), function(candidate) {
12815
+ var candidateText = String(candidate && candidate._newmarkMessageText || '').trim();
12816
+ if (candidateText && candidateText === wantedAnchorText) { anchor = candidate; return true; }
12817
+ return false;
12818
+ });
12819
+ }
12747
12820
  }
12748
12821
  if (anchor) {
12749
12822
  var pager = document.createElement('div');
@@ -13284,9 +13357,41 @@ function hydrateConversationBranchState(snapshot) {
13284
13357
  if (Object.prototype.hasOwnProperty.call(s, 'branchGroupId')) state.activeConversationBranchGroupId = String(s.branchGroupId || '');
13285
13358
  if (Object.prototype.hasOwnProperty.call(s, 'viewedBranchNodePath')) state.viewedConversationBranchNodePath = Array.isArray(s.viewedBranchNodePath) ? s.viewedBranchNodePath.map(String) : [];
13286
13359
  if (Object.prototype.hasOwnProperty.call(s, 'runtimeBranchNodePath')) state.runtimeConversationBranchNodePath = Array.isArray(s.runtimeBranchNodePath) ? s.runtimeBranchNodePath.map(String) : [];
13360
+ // 分支级缓存必须知道「当前浏览的是哪个节点」,否则换页后另一页的快照会覆盖本页。
13361
+ var viewedLeaf = String((state.viewedConversationBranchNodePath || []).slice(-1)[0] || state.activeConversationBranchId || '');
13362
+ rememberViewedBranchNodeId(currentConversationTarget(s.conversationId || activeConversationId()), viewedLeaf);
13287
13363
  rememberRuntimeConversationBranchPath(currentConversationTarget(s.conversationId || activeConversationId()));
13288
13364
  }
13289
13365
 
13366
+ // Apply the conversation's remembered provider-qualified model to the
13367
+ // composer selector. The backend snapshot is authoritative; a newer explicit
13368
+ // user selection made while the snapshot was in flight wins via the revision
13369
+ // guard passed by the activation caller.
13370
+ function applyConversationModelSelection(snapshot) {
13371
+ var nextModel = String(snapshot && snapshot.model || '').trim();
13372
+ if (!nextModel) return;
13373
+ state.model = nextModel;
13374
+ if (typeof snapshot.modelLabel === 'string' && snapshot.modelLabel) state.modelLabel = snapshot.modelLabel;
13375
+ state._syncedModel = nextModel;
13376
+ var sel = els['model-select'] || document.getElementById('model-select');
13377
+ if (!sel) {
13378
+ if (window.syncModelSelectSurface) window.syncModelSelectSurface();
13379
+ return;
13380
+ }
13381
+ var hasOption = Array.prototype.some.call(sel.options, function(option) {
13382
+ return String(option.value || '') === nextModel;
13383
+ });
13384
+ if (hasOption) {
13385
+ sel.value = nextModel;
13386
+ window.syncModelSelectSurface();
13387
+ } else if (window.refreshModelSelect) {
13388
+ // The remembered deployment is not in the current enabled catalog (for
13389
+ // example its provider was explicitly disabled). Reuse the existing
13390
+ // visibility fallback instead of displaying a model that cannot run.
13391
+ window.refreshModelSelect();
13392
+ }
13393
+ }
13394
+
13290
13395
  function applyConversationSnapshot(s, requestedConversationId, options) {
13291
13396
  if (!s) return;
13292
13397
  if (s.computerUse) {
@@ -13309,6 +13414,11 @@ function applyConversationSnapshot(s, requestedConversationId, options) {
13309
13414
  if (s && s.runtimeKey) registerRuntimeKey(snapshotTarget, s.runtimeKey);
13310
13415
  hydrateConversationDraftSnapshot(s, snapshotTarget);
13311
13416
  hydrateConversationBranchState(s);
13417
+ var modelRevisionMatches = !options || options.modelRevision === undefined
13418
+ || Number(state.modelSelectionRevision || 0) === Number(options.modelRevision || 0);
13419
+ if (modelRevisionMatches && isActiveConversationTarget(snapshotTarget)) {
13420
+ applyConversationModelSelection(s);
13421
+ }
13312
13422
  rebindQueueToRuntimeBranch(snapshotTarget);
13313
13423
  if (s && Array.isArray(s.workRuns) && !preserveReadableHistory) {
13314
13424
  var viewedBranchIdForSync = Array.isArray(s.viewedBranchNodePath) && s.viewedBranchNodePath.length
@@ -13411,10 +13521,54 @@ function optionQuestionKey(question, options, index) {
13411
13521
  return JSON.stringify({ index: index, question: question || '', options: (options || []).map(optionLabel) });
13412
13522
  }
13413
13523
 
13414
- function addOptionBlock(question, options, questionKey) {
13524
+ // dev-0.6.4 hotfix: 选项块携带归属会话,作答必须回到提问的那个会话。
13525
+ function optionBlockOwner(element) {
13526
+ var block = element && element.closest ? element.closest('[data-option-owner-conversation]') : null;
13527
+ if (!block) return currentConversationTarget();
13528
+ return {
13529
+ workspaceId: String(block.getAttribute('data-option-owner-workspace') || '') || runtimeWorkspaceId(''),
13530
+ conversationId: String(block.getAttribute('data-option-owner-conversation') || '') || activeConversationId()
13531
+ };
13532
+ }
13533
+
13534
+ function pendingOptionsForTarget(target) {
13535
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
13536
+ if (!owner) return [];
13537
+ if (!state.pendingOptionsByTarget) state.pendingOptionsByTarget = {};
13538
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
13539
+ return Array.isArray(state.pendingOptionsByTarget[key]) ? state.pendingOptionsByTarget[key] : [];
13540
+ }
13541
+
13542
+ function setPendingOptionsForTarget(options, target) {
13543
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
13544
+ if (!owner) return;
13545
+ if (!state.pendingOptionsByTarget) state.pendingOptionsByTarget = {};
13546
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
13547
+ state.pendingOptionsByTarget[key] = (Array.isArray(options) ? options : []).map(function(question) {
13548
+ return {
13549
+ id: String(question && question.id || ''),
13550
+ question: String(question && question.question || ''),
13551
+ options: (Array.isArray(question && question.options) ? question.options : []).map(function(option) {
13552
+ return typeof option === 'string' ? option : { label: String(option && option.label || ''), description: String(option && option.description || '') };
13553
+ })
13554
+ };
13555
+ });
13556
+ if (isActiveConversationTarget(owner)) state.pendingOptions = state.pendingOptionsByTarget[key];
13557
+ }
13558
+
13559
+ function addOptionBlock(question, options, questionKey, ownerAttributes) {
13415
13560
  var div = document.createElement('div');
13416
13561
  div.className = 'chat-msg assistant';
13417
13562
  div.setAttribute('data-option-question', encodeURIComponent(questionKey));
13563
+ if (ownerAttributes) {
13564
+ var ownerHolder = document.createElement('div');
13565
+ ownerHolder.innerHTML = '<span' + ownerAttributes + '></span>';
13566
+ var ownerNode = ownerHolder.firstElementChild;
13567
+ if (ownerNode) {
13568
+ div.setAttribute('data-option-owner-workspace', ownerNode.getAttribute('data-option-owner-workspace') || '');
13569
+ div.setAttribute('data-option-owner-conversation', ownerNode.getAttribute('data-option-owner-conversation') || '');
13570
+ }
13571
+ }
13418
13572
  var html = '<div class="option-block"><div class="option-question">' + esc(question) + '</div>';
13419
13573
  for (var i = 0; i < options.length; i++) {
13420
13574
  var label = optionLabel(options[i]);
@@ -13433,11 +13587,18 @@ function addOptionBlock(question, options, questionKey) {
13433
13587
  }
13434
13588
 
13435
13589
  function renderPendingOptionsInChat(options) {
13590
+ // dev-0.6.4 hotfix: 选项提问按会话归属。旧实现把全局 pendingOptions 渲染到当前
13591
+ // 对话区,后台会话的提问会“溢出”到前台对话,点击后还会用当前会话发送答案,
13592
+ // 等于把提问激活给了另一个对话。
13593
+ if (options === undefined) options = pendingOptionsForTarget(currentConversationTarget());
13436
13594
  options = options || [];
13437
13595
  if (!options.length) return;
13596
+ var owner = currentConversationTarget();
13597
+ var ownerAttributes = ' data-option-owner-workspace="' + escAttr(String(owner.workspaceId || '')) + '" data-option-owner-conversation="' + escAttr(String(owner.conversationId || 'default')) + '"';
13438
13598
  var batchKey = JSON.stringify(options.map(function(q, index) { return optionQuestionKey(q.question || q.id || '', q.options || [], index); }));
13439
- if (state.pendingOptionBatchKey !== batchKey) {
13440
- state.pendingOptionBatchKey = batchKey;
13599
+ var batchKeyWithOwner = String(owner.workspaceId || '') + '::' + String(owner.conversationId || 'default') + '::' + batchKey;
13600
+ if (state.pendingOptionBatchKey !== batchKeyWithOwner) {
13601
+ state.pendingOptionBatchKey = batchKeyWithOwner;
13441
13602
  state.pendingOptionAnswers = {};
13442
13603
  }
13443
13604
  for (var i = 0; i < options.length; i++) {
@@ -13447,7 +13608,7 @@ function renderPendingOptionsInChat(options) {
13447
13608
  var encodedKey = encodeURIComponent(key);
13448
13609
  if (els['chat-area'] && els['chat-area'].querySelector('[data-option-question="' + encodedKey + '"]')) continue;
13449
13610
  state.renderedOptionKeys[key] = true;
13450
- addOptionBlock(q.question || q.id || t('status.question'), q.options || [], key);
13611
+ addOptionBlock(q.question || q.id || t('status.question'), q.options || [], key, ownerAttributes);
13451
13612
  }
13452
13613
  }
13453
13614
 
@@ -13506,6 +13667,15 @@ function handlePlanExecutionChoice(questionKey, opt) {
13506
13667
 
13507
13668
  window.optionSelected = function(questionKey, opt, button) {
13508
13669
  if (!questionKey || state.pendingOptionAnswers[questionKey] !== undefined) return;
13670
+ // dev-0.6.4 hotfix: 作答必须回到提问的那个会话;后台会话的提问不允许借用
13671
+ // 当前对话的输入框发送(旧实现用 window.sendMessage() 直接发到当前会话)。
13672
+ var answerOwner = optionBlockOwner(button);
13673
+ if (!isActiveConversationTarget(answerOwner)) {
13674
+ showUiNotice(currentLang() === 'zh'
13675
+ ? '该提问属于另一个对话,请先切换到那个对话再作答。'
13676
+ : 'That question belongs to another conversation; switch to it before answering.', 'error', 'option-owner-mismatch');
13677
+ return;
13678
+ }
13509
13679
  if (state.mode === 'plan' && handlePlanExecutionChoice(questionKey, opt)) {
13510
13680
  var planBlock = button && button.closest ? button.closest('[data-option-question]') : null;
13511
13681
  if (planBlock) Array.from(planBlock.querySelectorAll('.option-btn')).forEach(function(candidate) {
@@ -13522,7 +13692,7 @@ window.optionSelected = function(questionKey, opt, button) {
13522
13692
  candidate.classList.toggle('selected', candidate === button);
13523
13693
  });
13524
13694
  }
13525
- var questions = state.pendingOptions || [];
13695
+ var questions = pendingOptionsForTarget(answerOwner);
13526
13696
  var answered = questions.map(function(q, index) {
13527
13697
  var key = optionQuestionKey(q.question || q.id || '', q.options || [], index);
13528
13698
  return { question: q.question || q.id || t('status.question'), answer: state.pendingOptionAnswers[key] };
@@ -13543,6 +13713,13 @@ window.optionSelected = function(questionKey, opt, button) {
13543
13713
  };
13544
13714
 
13545
13715
  window.optionManualSubmitted = function(element) {
13716
+ var manualOwner = optionBlockOwner(element);
13717
+ if (!isActiveConversationTarget(manualOwner)) {
13718
+ showUiNotice(currentLang() === 'zh'
13719
+ ? '该提问属于另一个对话,请先切换到那个对话再作答。'
13720
+ : 'That question belongs to another conversation; switch to it before answering.', 'error', 'option-owner-mismatch');
13721
+ return;
13722
+ }
13546
13723
  var manual = element && element.closest ? element.closest('.option-manual') : null;
13547
13724
  if (!manual) return;
13548
13725
  var input = manual.querySelector('.option-manual-input');
@@ -14443,6 +14620,7 @@ function applyModelFallbackToInputSurface(event) {
14443
14620
  var nextValue = modelDeploymentValueForName(f.to, f.providerId || '');
14444
14621
  if (!nextValue || String(state.model || '') === nextValue) return;
14445
14622
  state.model = nextValue;
14623
+ state.modelSelectionRevision = Number(state.modelSelectionRevision || 0) + 1;
14446
14624
  state.modelLabel = '';
14447
14625
  // 不预置 _syncedModel:下一次发送前的 syncConversationExecutionState 会把
14448
14626
  // 回退模型正式同步到后端 host agent(并持久化为该对话的首选模型)。
@@ -17208,6 +17386,11 @@ function bindBackendQueuedRequestToTarget(text, target, occurrence) {
17208
17386
  request.createdAt = queueItem.createdAt;
17209
17387
  request.runId = queueItem.runId;
17210
17388
  request.images = (queueItem.images || []).map(function(image) { return Object.assign({}, image); });
17389
+ if (Array.isArray(queueItem.branchPath) && queueItem.branchPath.length) {
17390
+ request.branchPath = queueItem.branchPath.join('>');
17391
+ } else if (queueItem.branchNodeId) {
17392
+ request.branchPath = String(queueItem.branchNodeId);
17393
+ }
17211
17394
  }
17212
17395
  return request;
17213
17396
  }
@@ -17229,13 +17412,24 @@ function queueIndexesForTarget(target) {
17229
17412
  var requests = state.nextQueueRequests || [];
17230
17413
  for (var index = 0; index < queue.length; index++) {
17231
17414
  if (!queuedRequestMatchesTarget(requests[index], target)) continue;
17232
- var requestBranchPath = String(requests[index] && requests[index].branchPath || '');
17233
- var runtimeBranchPath = queueBranchPathForTarget(target, 'runtime');
17234
- if (!requestBranchPath || !runtimeBranchPath || requestBranchPath === runtimeBranchPath) indexes.push(index);
17415
+ // dev-0.6.4: 排队行永远可见。它属于「受理时捕获的分支」,但它是这个会话里
17416
+ // 真实存在的用户输入;旧逻辑按当前运行分支过滤,分页之后整队行被隐形,
17417
+ // 用户既看不到内容也无法恢复。可见性与行绑定的分支解耦(分支只决定它
17418
+ // 何时、在哪一页执行)。
17419
+ indexes.push(index);
17235
17420
  }
17236
17421
  return indexes;
17237
17422
  }
17238
17423
 
17424
+ // 行绑定的分支与当前运行分支不一致时,该行会等待(内核会暂停而不是改投)。
17425
+ // 这个状态必须在界面上可见,用户才能切回对应分页再恢复队列。
17426
+ function queueRequestBranchMismatch(target, request) {
17427
+ var requestBranchPath = String(request && request.branchPath || '');
17428
+ var runtimeBranchPath = queueBranchPathForTarget(target, 'runtime');
17429
+ if (!requestBranchPath || !runtimeBranchPath) return false;
17430
+ return requestBranchPath !== runtimeBranchPath;
17431
+ }
17432
+
17239
17433
  function queueIndexesForRuntimeTarget(target) {
17240
17434
  var indexes = [];
17241
17435
  var queue = state.nextQueue || [];
@@ -17268,7 +17462,9 @@ function rebindQueueToRuntimeBranch(target) {
17268
17462
  if (!runtimePath || !key) return;
17269
17463
  (state.nextQueueRequests || []).forEach(function(request) {
17270
17464
  if (!request || !queuedRequestMatchesTarget(request, target)) return;
17271
- request.branchPath = runtimePath;
17465
+ // dev-0.6.4: 只刷新会话 runtime 身份,绝不改写受理时捕获的分支路径。
17466
+ // 静默改绑会把排队输入从用户提交的分页挪到另一条分支,正是
17467
+ // 「跨分支复用/串分支」必须禁止的行为。
17272
17468
  request.runtimeKey = key;
17273
17469
  });
17274
17470
  }
@@ -17333,6 +17529,21 @@ function setQueueItemsForTarget(items, target) {
17333
17529
  runId: String(item && item.runId || ''),
17334
17530
  createdAt: String(item && item.createdAt || ''),
17335
17531
  images: (Array.isArray(item && item.images) ? item.images : []).map(function(image) { return Object.assign({}, image); }),
17532
+ // dev-0.6.4: 保留受理时捕获的分支身份。旧实现只保留展示字段,排队行在
17533
+ // 重新投影时会被默认绑到「当前运行分支」,等于静默改绑到另一条分页,
17534
+ // 也让「等待所属分页」的状态永远无法显示。
17535
+ branchNodeId: String(item && item.branchNodeId || ''),
17536
+ branchPath: Array.isArray(item && item.branchPath)
17537
+ ? item.branchPath.map(function(value) { return String(value || ''); }).filter(Boolean)
17538
+ : (item && item.branchPath ? [String(item.branchPath)] : []),
17539
+ ledgerBranchId: String(item && item.ledgerBranchId || ''),
17540
+ targetRuntimeKey: String(item && item.targetRuntimeKey || ''),
17541
+ workspaceKey: String(item && item.workspaceKey || ''),
17542
+ modelSelection: String(item && item.modelSelection || ''),
17543
+ // dev-0.6.4 hotfix: 保留账本给出的受阻原因,界面才能显示“被失败前置阻断”
17544
+ // 并提供显式的修复入口。
17545
+ waitingReason: String(item && item.waitingReason || ''),
17546
+ blocked: item && item.blocked === true,
17336
17547
  };
17337
17548
  });
17338
17549
  }
@@ -17368,7 +17579,23 @@ function applyConversationCommandSnapshot(snapshot, target) {
17368
17579
  if (!state.queuePausedByTarget) state.queuePausedByTarget = {};
17369
17580
  state.queuePausedByTarget[key] = snapshot.queuePaused;
17370
17581
  }
17371
- if (Array.isArray(snapshot.queueItems)) setQueueItemsForTarget(snapshot.queueItems, target);
17582
+ if (Array.isArray(snapshot.queueItems)) {
17583
+ // dev-0.6.4 hotfix: 排队行真正进入对话(出队)时,后端会把这一轮写成正式用户
17584
+ // 消息,但渲染端不会收到发送回执,因此此前对话区一直不显示这条用户输入。
17585
+ // 这里比较权威队列的条数变化,检测到出队就刷新一次转录。
17586
+ var previousQueueItems = queueItemsForTarget(target);
17587
+ var previousQueueIds = previousQueueItems.map(function(item) { return String(item && item.id || ''); }).filter(Boolean);
17588
+ var nextQueueIds = snapshot.queueItems.map(function(item) { return String(item && item.id || ''); }).filter(Boolean);
17589
+ var drainedIds = previousQueueIds.filter(function(id) { return nextQueueIds.indexOf(id) < 0; });
17590
+ setQueueItemsForTarget(snapshot.queueItems, target);
17591
+ if (drainedIds.length) scheduleDrainedTurnTranscriptRefresh(target, drainedIds);
17592
+ }
17593
+ if (Array.isArray(snapshot.pendingOptions)) setPendingOptionsForTarget(snapshot.pendingOptions, target);
17594
+ // dev-0.6.4: goal 停止/清除后,任何携带 goal 字段的权威快照都要同步前端显示。
17595
+ if (Object.prototype.hasOwnProperty.call(snapshot, 'goal') && isActiveConversationTarget(target)) {
17596
+ applyReturnedGoalState(snapshot);
17597
+ if (window.renderInputStack) window.renderInputStack();
17598
+ }
17372
17599
  var queue = snapshot.queued && typeof snapshot.queued === 'object' ? snapshot.queued : snapshot.queue;
17373
17600
  if (queue && typeof queue === 'object') {
17374
17601
  setBackendQueueForTarget(queue, target);
@@ -17405,6 +17632,55 @@ function applyConversationCommandSnapshot(snapshot, target) {
17405
17632
  }
17406
17633
  }
17407
17634
 
17635
+ /**
17636
+ * dev-0.6.4 hotfix: 排队输入出队进入对话后,把权威转录刷新到对话区。
17637
+ *
17638
+ * 走发送路径的回合有本地乐观气泡 + 回执刷新;排队回合由内核在后台启动,渲染端
17639
+ * 既没有乐观气泡也没有回执,于是用户输入在对话区“消失”。这里在检测到排队行
17640
+ * 出队时按目标做一次去抖刷新:只在当前前台会话、且没有渲染端发送在飞时执行,
17641
+ * 避免与发送回执互相覆盖。
17642
+ */
17643
+ function scheduleDrainedTurnTranscriptRefresh(target, drainedIds) {
17644
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
17645
+ if (!owner || !api.getState) return;
17646
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
17647
+ if (!isActiveConversationTarget(owner)) return;
17648
+ if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[key]) return;
17649
+ if (!state.pendingDrainRefreshByTarget) state.pendingDrainRefreshByTarget = {};
17650
+ if (state.pendingDrainRefreshByTarget[key]) return;
17651
+ // 出队事件早于正式用户消息落盘,因此按几个时间点各刷新一次:首次拿到用户输入,
17652
+ // 后续拿到助手回复;全部完成后清除登记,下一次出队可再次触发。
17653
+ state.pendingDrainRefreshByTarget[key] = { ids: (drainedIds || []).slice(), timers: [] };
17654
+ var pending = state.pendingDrainRefreshByTarget[key];
17655
+ [150, 1200, 3000].forEach(function(delay, index, all) {
17656
+ pending.timers.push(setTimeout(function() {
17657
+ refreshConversationTranscriptFromBackend(owner);
17658
+ if (index === all.length - 1 && state.pendingDrainRefreshByTarget) delete state.pendingDrainRefreshByTarget[key];
17659
+ }, delay));
17660
+ });
17661
+ }
17662
+
17663
+ function refreshConversationTranscriptFromBackend(target) {
17664
+ var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
17665
+ if (!owner || !api.getState || !isActiveConversationTarget(owner)) return Promise.resolve(null);
17666
+ var key = runtimeKeyFor(owner.workspaceId, owner.conversationId);
17667
+ if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[key]) return Promise.resolve(null);
17668
+ return Promise.resolve(api.getState(owner)).then(function(snapshot) {
17669
+ if (!snapshot || typeof snapshot !== 'object') return null;
17670
+ if (!isActiveConversationTarget(owner)) return null;
17671
+ if (state.activeSendCallsByTarget && state.activeSendCallsByTarget[key]) return null;
17672
+ // dev-0.6.4: goal 完成/停止后,前端必须在收到权威快照时同步清除 goal 显示。
17673
+ applyReturnedGoalState(snapshot);
17674
+ if (Array.isArray(snapshot.workRuns)) syncWorkRunsSnapshot(snapshot.workRuns, owner);
17675
+ if (Array.isArray(snapshot.chatMessages)) {
17676
+ hydrateConversationBranchState(snapshot);
17677
+ cacheConversationMessages(snapshot.chatMessages, owner);
17678
+ renderChatMessages(snapshot.chatMessages, owner);
17679
+ }
17680
+ return snapshot;
17681
+ }).catch(function() { return null; });
17682
+ }
17683
+
17408
17684
  window.queueAction = function(action, input, target) {
17409
17685
  if (!api.queueAction) return Promise.resolve({ ok: false, error: 'queueAction unavailable' });
17410
17686
  var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
@@ -17505,6 +17781,15 @@ window.renderQueuePanel = function() {
17505
17781
  var visibleIndexes = queueIndexesForTarget(target);
17506
17782
  var count = visibleIndexes.length;
17507
17783
  panel.style.display = count ? '' : 'none';
17784
+ // dev-0.6.4: 新排队输入必须被看见。旧行为把队列面板默认折叠,配合按分支
17785
+ // 过滤的隐形逻辑,用户提交的排队内容在界面上完全不出现。条数增加时自动
17786
+ // 展开,用户手动折叠在条数不变时仍然有效。
17787
+ if (!state.queueSeenCountsByTarget) state.queueSeenCountsByTarget = {};
17788
+ // 仅用于「新增排队行时自动展开面板」的本地记账键,不参与执行身份判定。
17789
+ var seenKey = String(target.workspaceId || '') + '::' + String(target.conversationId || 'default');
17790
+ var seenCount = Number(state.queueSeenCountsByTarget[seenKey] || 0);
17791
+ if (count > seenCount) state.queueCollapsed = false;
17792
+ state.queueSeenCountsByTarget[seenKey] = count;
17508
17793
  panel.classList.toggle('collapsed', !!state.queueCollapsed);
17509
17794
  label.textContent = t('queue.title') + ' ' + count;
17510
17795
  var pauseToggle = document.getElementById('queue-pause-btn');
@@ -17548,17 +17833,73 @@ window.renderQueuePanel = function() {
17548
17833
  '<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>' +
17549
17834
  '<button class="stack-icon-btn" onclick="window.focusQueueItem(' + eventIndex + ')" title="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</button>' +
17550
17835
  '<button class="stack-icon-btn danger" onclick="window.deleteQueueItem(' + eventIndex + ')" title="' + escAttr(t('common.delete')) + '">' + iconSvg('trash', t('common.delete'), 'tiny') + '</button>' : '';
17551
- html += '<div class="queue-item" data-queue-index="' + queueIndex + '"' + identityAttributes + dragAttributes + '>' +
17836
+ // 绑定了另一条分页的行会等待而不是改投,这个状态必须在界面上显式可见,
17837
+ // 否则用户会以为排队输入被丢弃。
17838
+ var waitingForBranch = queueRequestBranchMismatch(target, request);
17839
+ var waitingLabel = currentLang() === 'zh'
17840
+ ? '等待它所属的分页:切回该分页后队列可恢复执行'
17841
+ : 'Waiting for its own page: switch back to that page to resume the queue';
17842
+ // dev-0.6.4 hotfix: 被失败前置阻断的排队行必须显式可见,否则用户只会看到
17843
+ // “恢复后什么都不发生”。恢复队列时内核会把这些行重新挂到最后一个已提交 Build。
17844
+ var authoritativeItem = queueItemId
17845
+ ? queueItemsForTarget(target).find(function(item) { return item.id === queueItemId; })
17846
+ : null;
17847
+ var blockedRow = !!(authoritativeItem && (authoritativeItem.blocked === true || authoritativeItem.waitingReason));
17848
+ var blockedLabel = currentLang() === 'zh'
17849
+ ? '这一行被失败的前置 Build 阻断:按“恢复队列”会把它重新挂到最后一个已提交 Build 后执行'
17850
+ : 'Blocked by a failed predecessor: resuming the queue re-anchors this row to the last committed build';
17851
+ html += '<div class="queue-item" data-queue-index="' + queueIndex + '"' + identityAttributes
17852
+ + (waitingForBranch ? ' data-queue-branch-waiting="1" title="' + escAttr(waitingLabel) + '"' : '')
17853
+ + (blockedRow ? ' data-queue-blocked="1" title="' + escAttr(blockedLabel) + '"' : '')
17854
+ + dragAttributes + '>' +
17552
17855
  dragHandle +
17553
17856
  '<input class="queue-edit" value="' + escAttr(state.nextQueue[queueIndex] || '') + '"' + inputAttributes + '>' +
17554
17857
  '<span class="stack-actions">' + actions + '</span>' +
17555
17858
  '</div>';
17556
17859
  }
17557
17860
  list.innerHTML = html;
17861
+
17862
+ // dev-0.6.4 hotfix: 存在受阻行时提供显式修复入口。0.6.3 合约要求失败前置继续
17863
+ // 阻断后继(不静默跳过),因此修复必须是用户主动动作,并且可见、可审计。
17864
+ var blockedCount = 0;
17865
+ for (var blockedIndex = 0; blockedIndex < visibleIndexes.length; blockedIndex++) {
17866
+ var blockedRequest = state.nextQueueRequests[visibleIndexes[blockedIndex]];
17867
+ var blockedItemId = blockedRequest ? String(blockedRequest.queueItemId || '') : '';
17868
+ var blockedItem = blockedItemId
17869
+ ? queueItemsForTarget(target).find(function(item) { return item.id === blockedItemId; })
17870
+ : null;
17871
+ if (blockedItem && (blockedItem.blocked === true || blockedItem.waitingReason)) blockedCount++;
17872
+ }
17873
+ var headerActions = panel.querySelector('#queue-header .stack-actions');
17874
+ var repairButton = document.getElementById('queue-repair-btn');
17875
+ if (blockedCount) {
17876
+ if (!repairButton && headerActions) {
17877
+ repairButton = document.createElement('button');
17878
+ repairButton.className = 'stack-icon-btn';
17879
+ repairButton.id = 'queue-repair-btn';
17880
+ repairButton.setAttribute('type', 'button');
17881
+ repairButton.setAttribute('onclick', 'event.stopPropagation();window.repairBlockedQueue()');
17882
+ var pauseButton = document.getElementById('queue-pause-btn');
17883
+ if (pauseButton && pauseButton.parentNode === headerActions) headerActions.insertBefore(repairButton, pauseButton);
17884
+ else headerActions.appendChild(repairButton);
17885
+ }
17886
+ if (repairButton) {
17887
+ var repairLabel = currentLang() === 'zh' ? '修复受阻队列' : 'Repair blocked queue';
17888
+ repairButton.title = repairLabel;
17889
+ repairButton.innerHTML = iconSvg('wrench', repairLabel, 'tiny') + '<span class="icon-only-label">' + esc(repairLabel) + '</span>';
17890
+ }
17891
+ } else if (repairButton && repairButton.parentNode) {
17892
+ repairButton.remove();
17893
+ }
17558
17894
  };
17559
17895
 
17560
17896
  window.renderNextQueue = window.renderQueuePanel;
17561
17897
 
17898
+ // 显式修复受阻队列:把被失败前置阻断的排队行重新挂到最后一个已提交 Build。
17899
+ window.repairBlockedQueue = function() {
17900
+ return window.queueAction('repair_blocked', {}, currentConversationTarget());
17901
+ };
17902
+
17562
17903
  window.toggleQueuePanel = function() {
17563
17904
  state.queueCollapsed = !state.queueCollapsed;
17564
17905
  window.renderInputStack();
@@ -17920,7 +18261,15 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
17920
18261
  queuedModeRequest.goalDeclaration = true;
17921
18262
  queuedModeRequest.goalObjective = rawText;
17922
18263
  }
17923
- var queueResult = await api.sendMessage({ text: rawText, images: queuedModeRequest.images }, lockedTarget, {
18264
+ var queuedBranchPath = Array.isArray(queuedModeRequest.branchPath)
18265
+ ? queuedModeRequest.branchPath
18266
+ : String(queuedModeRequest.branchPath || '').split('>').filter(Boolean);
18267
+ var queueResult = await api.sendMessage({
18268
+ text: rawText,
18269
+ images: queuedModeRequest.images,
18270
+ branchPath: queuedBranchPath,
18271
+ branchNodeId: String(queuedBranchPath[queuedBranchPath.length - 1] || '')
18272
+ }, lockedTarget, {
17924
18273
  clientMessageId: queuedModeRequest.clientMessageId,
17925
18274
  inputMode: 'next',
17926
18275
  requestedMode: requestedMode,
@@ -18117,12 +18466,6 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18117
18466
  updateSubmitButtonState();
18118
18467
  var executionMode = opts.goalDeclaration || requestedMode === 'goal' ? 'build'
18119
18468
  : ((opts.forceBuild || idleBuildNextImmediate) && requestedMode === 'build' ? 'build' : requestedMode);
18120
- // The command freezes its mode and delivery policy; selector persistence
18121
- // must not rewrite the mode of an already running target before acceptance.
18122
- if (api.setModel && state.model !== state._syncedModel) {
18123
- var selectedModelResult = await api.setModel(state.model);
18124
- if (selectedModelResult != null) state._syncedModel = state.model;
18125
- }
18126
18469
  if (consumeProvisionalStop(lockedTarget, localRunId)) {
18127
18470
  // Stop/Escape won before the backend accepted the prompt. Do not send the
18128
18471
  // provider request after the user has already cancelled it.
@@ -18166,10 +18509,20 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18166
18509
  try {
18167
18510
  var activationBeforeSend = pendingConversationActivation(lockedTarget);
18168
18511
  if (activationBeforeSend) await activationBeforeSend;
18512
+ // Bind the model actually shown in the composer to the exact target
18513
+ // conversation after its activation snapshot restored the remembered
18514
+ // selection. This makes the visible provider/model authoritative for the
18515
+ // send and prevents a stale renderer selection from reaching another
18516
+ // conversation's remembered deployment.
18517
+ if (api.setModel && state.model && state.model !== state._syncedModel) {
18518
+ var selectedModelResult = await api.setModel(state.model);
18519
+ if (selectedModelResult != null) state._syncedModel = state.model;
18520
+ }
18169
18521
  var sendPromise = api.sendMessage(requestMessage, lockedTarget, {
18170
18522
  requestedMode: requestedMode, inputMode: effectiveInputMode,
18171
18523
  goalObjective: opts.goalObjective || '',
18172
- clientMessageId: String(requestMessage && requestMessage.clientMessageId || localRunId)
18524
+ clientMessageId: String(requestMessage && requestMessage.clientMessageId || localRunId),
18525
+ model: state.model
18173
18526
  });
18174
18527
  if (requestedMode === 'goal') activateSubmittedGoal(opts.goalObjective || rawText);
18175
18528
  var r = await sendPromise;
@@ -18906,7 +19259,7 @@ window.refreshRightStatus = function(options) {
18906
19259
  }
18907
19260
  if (s && s.subagents) state.subagents = s.subagents;
18908
19261
  if (s && s.fileDiffs) state.fileDiffs = s.fileDiffs;
18909
- if (s && s.pendingOptions) state.pendingOptions = s.pendingOptions;
19262
+ if (s && s.pendingOptions) setPendingOptionsForTarget(s.pendingOptions, computerTarget || currentConversationTarget(s.conversationId || requestedConversationId || activeConversationId()));
18910
19263
  // Bind the Flow takeover to backend per-conversation state. The takeover
18911
19264
  // for the requested target reflects exactly what the backend holds for that
18912
19265
  // conversation: a suspension (paused) or an active running Flow. It is
@@ -19792,7 +20145,9 @@ function applyReturnedGoalState(snapshot) {
19792
20145
  if (!snapshot || !Object.prototype.hasOwnProperty.call(snapshot, 'goal')) return;
19793
20146
  state.goalText = snapshot.goal && snapshot.goal.objective ? String(snapshot.goal.objective) : '';
19794
20147
  state.goalVisible = !!state.goalText;
19795
- state.goalPaused = !!(snapshot.goal && snapshot.goal.paused);
20148
+ // dev-0.6.4: goal 结束后端会停止/清空 goal,但前端此前只清 text/visible,paused
20149
+ // 残留会让 goal 条继续以“暂停”状态显示。这里在无 goal 时一并清除暂停态。
20150
+ state.goalPaused = state.goalVisible ? !!(snapshot.goal && snapshot.goal.paused) : false;
19796
20151
  if (!snapshot.goal && snapshot.mode === 'build') {
19797
20152
  state.mode = 'build';
19798
20153
  if (els['mode-select']) {
@@ -23965,6 +24320,7 @@ function syncBackendConversation() {
23965
24320
  }
23966
24321
  if (conv && api.ensureConversation) {
23967
24322
  var requestedTarget = currentConversationTarget(conv.id);
24323
+ var modelRevisionAtActivation = Number(state.modelSelectionRevision || 0);
23968
24324
  state.activeBackendConversationId = conv.id;
23969
24325
  var historyPromise = loadActiveConversationMessages(conv.id);
23970
24326
  requestedLoadGeneration = state.conversationLoadGeneration;
@@ -23973,7 +24329,7 @@ function syncBackendConversation() {
23973
24329
  var id = (s && s.conversationId) || conv.id;
23974
24330
  if (!requestIsCurrent() || String(id) !== requestedConversationId) return;
23975
24331
  state.activeBackendConversationId = id;
23976
- applyConversationSnapshot(s, id, { preserveReadableHistory: true });
24332
+ applyConversationSnapshot(s, id, { preserveReadableHistory: true, modelRevision: modelRevisionAtActivation });
23977
24333
  }).catch(function(error) {
23978
24334
  if (!requestIsCurrent()) return;
23979
24335
  showUiNotice(
@@ -24505,12 +24861,13 @@ window.newConversation = function(workspaceReference, branchCommunication) {
24505
24861
  renderConversations();
24506
24862
  els['chat-area'].innerHTML = '';
24507
24863
  var target = currentConversationTarget(id);
24864
+ var modelRevisionAtActivation = Number(state.modelSelectionRevision || 0);
24508
24865
  var activation = api.activateConversation ? api.activateConversation(target) : (api.ensureConversation ? api.ensureConversation(target) : Promise.resolve(null));
24509
24866
  var activationKey = runtimeKeyFor(target.workspaceId, target.conversationId);
24510
24867
  var activationReady = activation.then(function(s) {
24511
24868
  if (createdWorkspaceKey !== currentWorkspaceKey() || id !== String(activeConversationId() || 'default')) return;
24512
24869
  state.activeBackendConversationId = String((s && s.conversationId) || id);
24513
- if (s) applyConversationSnapshot(s, id, { preserveReadableHistory: true });
24870
+ if (s) applyConversationSnapshot(s, id, { preserveReadableHistory: true, modelRevision: modelRevisionAtActivation });
24514
24871
  if (branchCommunication && api.setConversationBranchCommunication) {
24515
24872
  api.setConversationBranchCommunication(target, true).then(function() {
24516
24873
  var convsNow = currentWorkspaceConversations();
@@ -28307,6 +28664,7 @@ function schedulePostStartupUiRendering() {
28307
28664
  var selectedContextGeneration = state.conversationLoadGeneration;
28308
28665
  var selectedContextModel = this.value;
28309
28666
  state.model = selectedContextModel;
28667
+ state.modelSelectionRevision = Number(state.modelSelectionRevision || 0) + 1;
28310
28668
  window.syncModelSelectSurface();
28311
28669
  if (state.model !== state._syncedModel) {
28312
28670
  var modelResult = await api.setModel(selectedContextModel);