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.
- package/dist/conversation-utility-host.bundle.cjs +2149 -475
- package/dist/core/agent.d.ts +37 -0
- package/dist/core/agent.js +104 -7
- package/dist/core/agentKernelRunner.js +37 -2
- package/dist/core/branchIdentity.d.ts +29 -0
- package/dist/core/branchIdentity.js +54 -0
- package/dist/core/continuation/contracts.d.ts +197 -0
- package/dist/core/continuation/contracts.js +72 -0
- package/dist/core/continuation/store.d.ts +206 -0
- package/dist/core/continuation/store.js +1077 -0
- package/dist/core/conversationKernel.d.ts +93 -2
- package/dist/core/conversationKernel.js +648 -36
- package/dist/core/requestContextEstimate.d.ts +5 -1
- package/dist/llm/provider.js +9 -2
- package/dist/main.js +40 -5
- package/dist/providers/chat-completions.adapter.js +6 -1
- package/dist/providers/responses.adapter.js +4 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/ui/index.html +413 -55
- package/dist/wsl-agent-host.bundle.cjs +2152 -478
- package/package.json +5 -2
package/dist/ui/index.html
CHANGED
|
@@ -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
|
-
|
|
9954
|
-
|
|
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
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11307
|
+
// 读取「当前浏览的分支节点」自己的 Build 列表。`workRunsByTarget` 只是
|
|
11308
|
+
// 兼容性的展示镜像,按会话共享会让另一个分页的运行结果覆盖本页(新发送的
|
|
11309
|
+
// 消息看起来消失),因此渲染路径一律走按分支节点存储的账本。
|
|
11310
|
+
var viewedBranchId = viewedBranchNodeIdFor(target);
|
|
11311
|
+
if (!viewedBranchId) {
|
|
11260
11312
|
var branchIds = conversationBranchIdsForTarget(target);
|
|
11261
|
-
|
|
11313
|
+
viewedBranchId = branchIds.viewed;
|
|
11262
11314
|
}
|
|
11263
|
-
return
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
13440
|
-
|
|
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 =
|
|
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
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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);
|