newmark-agent 0.6.3 → 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;
@@ -7211,6 +7224,8 @@ var state = {
7211
7224
  pendingOptions: [],
7212
7225
  pendingOptionAnswers: {},
7213
7226
  pendingOptionBatchKey: '',
7227
+ pendingOptionsByTarget: {},
7228
+ pendingOptionAnswersByTarget: {},
7214
7229
  promptAttachments: [],
7215
7230
  nextQueue: [],
7216
7231
  nextQueueRequests: [],
@@ -7221,6 +7236,7 @@ var state = {
7221
7236
  backendQueue: { steering: [], followUp: [] },
7222
7237
  backendQueuesByTarget: {},
7223
7238
  queueItemsByTarget: {},
7239
+ pendingDrainRefreshByTarget: {},
7224
7240
  defaultFlow: '',
7225
7241
  settingsActiveTab: 'general',
7226
7242
  globalAgentPrompt: '',
@@ -9954,8 +9970,12 @@ window.submitUserMessageEdit = async function(message, messageIndex, editedText,
9954
9970
  if (!isActiveConversationTarget(target)) return;
9955
9971
  clearAgentWorkEvents(conversationId);
9956
9972
  resetConversationWorkUi(conversationId, false);
9957
- state.backendQueue = setBackendQueueForTarget({ steering: [], followUp: [] }, target);
9958
- 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);
9959
9979
  state.conversationBranches = Array.isArray(result.branches) ? result.branches : [];
9960
9980
  state.conversationBranchGroups = Array.isArray(result.branchGroups) ? result.branchGroups : [];
9961
9981
  state.activeConversationBranchId = String(result.activeBranchId || '');
@@ -10654,6 +10674,47 @@ function runtimeKeyFor(workspaceId, conversationId) {
10654
10674
  return state.runtimeKeyAliases && state.runtimeKeyAliases[base] || base;
10655
10675
  }
10656
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
+
10657
10718
  function registerRuntimeKey(target, runtimeKey) {
10658
10719
  runtimeKey = String(runtimeKey || '');
10659
10720
  if (!target || !runtimeKey) return runtimeKeyFor(target && target.workspaceId, target && target.conversationId);
@@ -10811,6 +10872,11 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
10811
10872
  var messages = guideMessagesForTarget(target);
10812
10873
  var pending = Object.keys(messages).map(function(id) { return messages[id]; }).filter(function(item) {
10813
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;
10814
10880
  // A Guide already owned by its work run is rendered by the Build block
10815
10881
  // (expanded events or collapsed guide list), never as a separate trailing
10816
10882
  // message. Without this guard, collapse/expand rebuilds would re-add the
@@ -10829,26 +10895,6 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
10829
10895
  element.remove();
10830
10896
  element = null;
10831
10897
  }
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
10898
  if (element) applyGuideMessageMeta(element, item);
10853
10899
  else addMsg('user', item.content || 'Guide', 'guide', item.model || state.model, undefined, {
10854
10900
  clientMessageId: item.clientMessageId,
@@ -11258,13 +11304,15 @@ function workRunBeforeElement(run) {
11258
11304
 
11259
11305
  function workRunsForTarget(target) {
11260
11306
  target = target || currentConversationTarget();
11261
- var key = runtimeKeyFor(target.workspaceId, target.conversationId);
11262
- if (!state.workRunsByTarget) state.workRunsByTarget = {};
11263
- if (!state.workRunsByTarget[key]) {
11307
+ // 读取「当前浏览的分支节点」自己的 Build 列表。`workRunsByTarget` 只是
11308
+ // 兼容性的展示镜像,按会话共享会让另一个分页的运行结果覆盖本页(新发送的
11309
+ // 消息看起来消失),因此渲染路径一律走按分支节点存储的账本。
11310
+ var viewedBranchId = viewedBranchNodeIdFor(target);
11311
+ if (!viewedBranchId) {
11264
11312
  var branchIds = conversationBranchIdsForTarget(target);
11265
- state.workRunsByTarget[key] = workRunsForBranch(target, branchIds.viewed);
11313
+ viewedBranchId = branchIds.viewed;
11266
11314
  }
11267
- return state.workRunsByTarget[key];
11315
+ return workRunsForBranch(target, viewedBranchId);
11268
11316
  }
11269
11317
 
11270
11318
  function syncWorkRunsSnapshot(runs, target, branchId) {
@@ -11642,9 +11690,12 @@ function renderWorkRunGuideMessage(event) {
11642
11690
  var guideId = String(guide.guideId || event.guideId || '');
11643
11691
  var messageIndex = Number(state.guideMessageIndexByClientId && state.guideMessageIndexByClientId[clientMessageId]);
11644
11692
  var indexed = Number.isFinite(messageIndex) && messageIndex >= 0;
11693
+ // dev-0.6.4: 时间线里的 Guide 记录不再提供编辑入口。Guide 是当前 Build 的
11694
+ // 历史干预记录,改写它既不会重放 Build,也会让分支归属变得含糊;这里只保留
11695
+ // 复制动作,消息索引属性仍然写回,供锚点/诊断使用。
11645
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 + '"' : '') + '>' +
11646
11697
  '<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>' +
11698
+ '<span class="guide-receipt-status ' + escAttr(status) + '">Guide · ' + esc(guideUiStatusLabel(status)) + '</span>' + messageActionsHtml('user', content, -1) + '</div>' +
11648
11699
  '<div class="msg-body">' + renderMessageContent(content) + '</div></div>';
11649
11700
  }
11650
11701
 
@@ -12173,6 +12224,12 @@ function applyAgentWorkEventToRun(event) {
12173
12224
  if (run.userToggled !== true) run.expanded = run.status === 'error';
12174
12225
  }
12175
12226
  }
12227
+ // dev-0.6.4 hotfix: 排队出队的回合结束时,把权威转录(助手回复、最终用户行)
12228
+ // 刷进对话区。正常发送路径有自己的回执刷新;这里只针对后台出队的运行,
12229
+ // 且由 scheduleDrainedTurnTranscriptRefresh 做去抖与“无发送在飞”保护。
12230
+ if (['done', 'error', 'interrupted', 'force_interrupted'].indexOf(type) >= 0) {
12231
+ scheduleDrainedTurnTranscriptRefresh(target, []);
12232
+ }
12176
12233
  if (isActiveConversationTarget(target) && (!branchIds.viewed || !eventBranchId || branchIds.viewed === eventBranchId)) {
12177
12234
  state.workRunsByTarget[runtimeKeyFor(target.workspaceId, target.conversationId)] = runs;
12178
12235
  var existingElement = findWorkRunElement(run);
@@ -12455,7 +12512,8 @@ function renderPersistedToolMessage(message) {
12455
12512
 
12456
12513
  function conversationMessageCache(target) {
12457
12514
  var scopedTarget = target || currentConversationTarget();
12458
- var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
12515
+ // 分支级缓存:分页切换时另一个页面的消息快照绝不能覆盖当前页。
12516
+ var key = branchConversationViewKey(scopedTarget);
12459
12517
  if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
12460
12518
  return Array.isArray(state.conversationMessagesByTarget[key])
12461
12519
  ? state.conversationMessagesByTarget[key].map(function(message) { return Object.assign({}, message); })
@@ -12464,7 +12522,7 @@ function conversationMessageCache(target) {
12464
12522
 
12465
12523
  function cacheConversationMessages(messages, target) {
12466
12524
  var scopedTarget = target || currentConversationTarget();
12467
- var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
12525
+ var key = branchConversationViewKey(scopedTarget);
12468
12526
  var cloned = Array.isArray(messages) ? messages.map(function(message) { return Object.assign({}, message); }) : [];
12469
12527
  if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
12470
12528
  state.conversationMessagesByTarget[key] = cloned;
@@ -12745,9 +12803,20 @@ function renderConversationBranchPagers() {
12745
12803
  if (matched) anchor = candidate;
12746
12804
  return matched;
12747
12805
  });
12748
- if (!anchor && !sourceMessageId && !sourceGuideId) {
12806
+ if (!anchor) {
12807
+ // dev-0.6.4: 分页锚点回退。Build 开头的用户消息可能以内联 Build/Guide
12808
+ // 卡片渲染,没有写回 data-message-id;旧逻辑此时直接放弃,导致「从 build
12809
+ // 开头编辑」创建的分页看不到分页条。现在按渲染索引,再按锚点文本回退。
12749
12810
  var sourceMessageIndex = Math.max(0, Number(branchGroup.sourceMessageIndex === undefined ? activeBranch.sourceMessageIndex : branchGroup.sourceMessageIndex));
12750
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
+ }
12751
12820
  }
12752
12821
  if (anchor) {
12753
12822
  var pager = document.createElement('div');
@@ -13288,6 +13357,9 @@ function hydrateConversationBranchState(snapshot) {
13288
13357
  if (Object.prototype.hasOwnProperty.call(s, 'branchGroupId')) state.activeConversationBranchGroupId = String(s.branchGroupId || '');
13289
13358
  if (Object.prototype.hasOwnProperty.call(s, 'viewedBranchNodePath')) state.viewedConversationBranchNodePath = Array.isArray(s.viewedBranchNodePath) ? s.viewedBranchNodePath.map(String) : [];
13290
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);
13291
13363
  rememberRuntimeConversationBranchPath(currentConversationTarget(s.conversationId || activeConversationId()));
13292
13364
  }
13293
13365
 
@@ -13449,10 +13521,54 @@ function optionQuestionKey(question, options, index) {
13449
13521
  return JSON.stringify({ index: index, question: question || '', options: (options || []).map(optionLabel) });
13450
13522
  }
13451
13523
 
13452
- 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) {
13453
13560
  var div = document.createElement('div');
13454
13561
  div.className = 'chat-msg assistant';
13455
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
+ }
13456
13572
  var html = '<div class="option-block"><div class="option-question">' + esc(question) + '</div>';
13457
13573
  for (var i = 0; i < options.length; i++) {
13458
13574
  var label = optionLabel(options[i]);
@@ -13471,11 +13587,18 @@ function addOptionBlock(question, options, questionKey) {
13471
13587
  }
13472
13588
 
13473
13589
  function renderPendingOptionsInChat(options) {
13590
+ // dev-0.6.4 hotfix: 选项提问按会话归属。旧实现把全局 pendingOptions 渲染到当前
13591
+ // 对话区,后台会话的提问会“溢出”到前台对话,点击后还会用当前会话发送答案,
13592
+ // 等于把提问激活给了另一个对话。
13593
+ if (options === undefined) options = pendingOptionsForTarget(currentConversationTarget());
13474
13594
  options = options || [];
13475
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')) + '"';
13476
13598
  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;
13599
+ var batchKeyWithOwner = String(owner.workspaceId || '') + '::' + String(owner.conversationId || 'default') + '::' + batchKey;
13600
+ if (state.pendingOptionBatchKey !== batchKeyWithOwner) {
13601
+ state.pendingOptionBatchKey = batchKeyWithOwner;
13479
13602
  state.pendingOptionAnswers = {};
13480
13603
  }
13481
13604
  for (var i = 0; i < options.length; i++) {
@@ -13485,7 +13608,7 @@ function renderPendingOptionsInChat(options) {
13485
13608
  var encodedKey = encodeURIComponent(key);
13486
13609
  if (els['chat-area'] && els['chat-area'].querySelector('[data-option-question="' + encodedKey + '"]')) continue;
13487
13610
  state.renderedOptionKeys[key] = true;
13488
- addOptionBlock(q.question || q.id || t('status.question'), q.options || [], key);
13611
+ addOptionBlock(q.question || q.id || t('status.question'), q.options || [], key, ownerAttributes);
13489
13612
  }
13490
13613
  }
13491
13614
 
@@ -13544,6 +13667,15 @@ function handlePlanExecutionChoice(questionKey, opt) {
13544
13667
 
13545
13668
  window.optionSelected = function(questionKey, opt, button) {
13546
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
+ }
13547
13679
  if (state.mode === 'plan' && handlePlanExecutionChoice(questionKey, opt)) {
13548
13680
  var planBlock = button && button.closest ? button.closest('[data-option-question]') : null;
13549
13681
  if (planBlock) Array.from(planBlock.querySelectorAll('.option-btn')).forEach(function(candidate) {
@@ -13560,7 +13692,7 @@ window.optionSelected = function(questionKey, opt, button) {
13560
13692
  candidate.classList.toggle('selected', candidate === button);
13561
13693
  });
13562
13694
  }
13563
- var questions = state.pendingOptions || [];
13695
+ var questions = pendingOptionsForTarget(answerOwner);
13564
13696
  var answered = questions.map(function(q, index) {
13565
13697
  var key = optionQuestionKey(q.question || q.id || '', q.options || [], index);
13566
13698
  return { question: q.question || q.id || t('status.question'), answer: state.pendingOptionAnswers[key] };
@@ -13581,6 +13713,13 @@ window.optionSelected = function(questionKey, opt, button) {
13581
13713
  };
13582
13714
 
13583
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
+ }
13584
13723
  var manual = element && element.closest ? element.closest('.option-manual') : null;
13585
13724
  if (!manual) return;
13586
13725
  var input = manual.querySelector('.option-manual-input');
@@ -17273,13 +17412,24 @@ function queueIndexesForTarget(target) {
17273
17412
  var requests = state.nextQueueRequests || [];
17274
17413
  for (var index = 0; index < queue.length; index++) {
17275
17414
  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);
17415
+ // dev-0.6.4: 排队行永远可见。它属于「受理时捕获的分支」,但它是这个会话里
17416
+ // 真实存在的用户输入;旧逻辑按当前运行分支过滤,分页之后整队行被隐形,
17417
+ // 用户既看不到内容也无法恢复。可见性与行绑定的分支解耦(分支只决定它
17418
+ // 何时、在哪一页执行)。
17419
+ indexes.push(index);
17279
17420
  }
17280
17421
  return indexes;
17281
17422
  }
17282
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
+
17283
17433
  function queueIndexesForRuntimeTarget(target) {
17284
17434
  var indexes = [];
17285
17435
  var queue = state.nextQueue || [];
@@ -17312,7 +17462,9 @@ function rebindQueueToRuntimeBranch(target) {
17312
17462
  if (!runtimePath || !key) return;
17313
17463
  (state.nextQueueRequests || []).forEach(function(request) {
17314
17464
  if (!request || !queuedRequestMatchesTarget(request, target)) return;
17315
- request.branchPath = runtimePath;
17465
+ // dev-0.6.4: 只刷新会话 runtime 身份,绝不改写受理时捕获的分支路径。
17466
+ // 静默改绑会把排队输入从用户提交的分页挪到另一条分支,正是
17467
+ // 「跨分支复用/串分支」必须禁止的行为。
17316
17468
  request.runtimeKey = key;
17317
17469
  });
17318
17470
  }
@@ -17377,6 +17529,21 @@ function setQueueItemsForTarget(items, target) {
17377
17529
  runId: String(item && item.runId || ''),
17378
17530
  createdAt: String(item && item.createdAt || ''),
17379
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,
17380
17547
  };
17381
17548
  });
17382
17549
  }
@@ -17412,7 +17579,23 @@ function applyConversationCommandSnapshot(snapshot, target) {
17412
17579
  if (!state.queuePausedByTarget) state.queuePausedByTarget = {};
17413
17580
  state.queuePausedByTarget[key] = snapshot.queuePaused;
17414
17581
  }
17415
- 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
+ }
17416
17599
  var queue = snapshot.queued && typeof snapshot.queued === 'object' ? snapshot.queued : snapshot.queue;
17417
17600
  if (queue && typeof queue === 'object') {
17418
17601
  setBackendQueueForTarget(queue, target);
@@ -17449,6 +17632,55 @@ function applyConversationCommandSnapshot(snapshot, target) {
17449
17632
  }
17450
17633
  }
17451
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
+
17452
17684
  window.queueAction = function(action, input, target) {
17453
17685
  if (!api.queueAction) return Promise.resolve({ ok: false, error: 'queueAction unavailable' });
17454
17686
  var owner = normalizeQueuedConversationTarget(target || currentConversationTarget());
@@ -17549,6 +17781,15 @@ window.renderQueuePanel = function() {
17549
17781
  var visibleIndexes = queueIndexesForTarget(target);
17550
17782
  var count = visibleIndexes.length;
17551
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;
17552
17793
  panel.classList.toggle('collapsed', !!state.queueCollapsed);
17553
17794
  label.textContent = t('queue.title') + ' ' + count;
17554
17795
  var pauseToggle = document.getElementById('queue-pause-btn');
@@ -17592,17 +17833,73 @@ window.renderQueuePanel = function() {
17592
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>' +
17593
17834
  '<button class="stack-icon-btn" onclick="window.focusQueueItem(' + eventIndex + ')" title="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</button>' +
17594
17835
  '<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 + '>' +
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 + '>' +
17596
17855
  dragHandle +
17597
17856
  '<input class="queue-edit" value="' + escAttr(state.nextQueue[queueIndex] || '') + '"' + inputAttributes + '>' +
17598
17857
  '<span class="stack-actions">' + actions + '</span>' +
17599
17858
  '</div>';
17600
17859
  }
17601
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
+ }
17602
17894
  };
17603
17895
 
17604
17896
  window.renderNextQueue = window.renderQueuePanel;
17605
17897
 
17898
+ // 显式修复受阻队列:把被失败前置阻断的排队行重新挂到最后一个已提交 Build。
17899
+ window.repairBlockedQueue = function() {
17900
+ return window.queueAction('repair_blocked', {}, currentConversationTarget());
17901
+ };
17902
+
17606
17903
  window.toggleQueuePanel = function() {
17607
17904
  state.queueCollapsed = !state.queueCollapsed;
17608
17905
  window.renderInputStack();
@@ -18962,7 +19259,7 @@ window.refreshRightStatus = function(options) {
18962
19259
  }
18963
19260
  if (s && s.subagents) state.subagents = s.subagents;
18964
19261
  if (s && s.fileDiffs) state.fileDiffs = s.fileDiffs;
18965
- if (s && s.pendingOptions) state.pendingOptions = s.pendingOptions;
19262
+ if (s && s.pendingOptions) setPendingOptionsForTarget(s.pendingOptions, computerTarget || currentConversationTarget(s.conversationId || requestedConversationId || activeConversationId()));
18966
19263
  // Bind the Flow takeover to backend per-conversation state. The takeover
18967
19264
  // for the requested target reflects exactly what the backend holds for that
18968
19265
  // conversation: a suspension (paused) or an active running Flow. It is
@@ -19848,7 +20145,9 @@ function applyReturnedGoalState(snapshot) {
19848
20145
  if (!snapshot || !Object.prototype.hasOwnProperty.call(snapshot, 'goal')) return;
19849
20146
  state.goalText = snapshot.goal && snapshot.goal.objective ? String(snapshot.goal.objective) : '';
19850
20147
  state.goalVisible = !!state.goalText;
19851
- 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;
19852
20151
  if (!snapshot.goal && snapshot.mode === 'build') {
19853
20152
  state.mode = 'build';
19854
20153
  if (els['mode-select']) {