diff --git a/agentchatbus-ts/package-lock.json b/agentchatbus-ts/package-lock.json
index 01915d6..03e2fc4 100644
--- a/agentchatbus-ts/package-lock.json
+++ b/agentchatbus-ts/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "agentchatbus-ts",
- "version": "0.2.30",
+ "version": "0.2.31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agentchatbus-ts",
- "version": "0.2.30",
+ "version": "0.2.31",
"dependencies": {
"@fastify/multipart": "^9.0.3",
"@fastify/static": "^9.0.0",
diff --git a/agentchatbus-ts/package.json b/agentchatbus-ts/package.json
index 7321909..ca91e5f 100644
--- a/agentchatbus-ts/package.json
+++ b/agentchatbus-ts/package.json
@@ -1,6 +1,6 @@
{
"name": "agentchatbus-ts",
- "version": "0.2.30",
+ "version": "0.2.31",
"private": true,
"description": "Initial TypeScript backend workspace for AgentChatBus",
"type": "module",
diff --git a/agentchatbus-ts/src/core/config/env.ts b/agentchatbus-ts/src/core/config/env.ts
index 6240bda..bc7895b 100644
--- a/agentchatbus-ts/src/core/config/env.ts
+++ b/agentchatbus-ts/src/core/config/env.ts
@@ -29,7 +29,7 @@ export {
import { getConfig } from "./registry.js";
import type { AppConfig } from "./registry.js";
-export const BUS_VERSION = "0.2.30";
+export const BUS_VERSION = "0.2.31";
export const ADMIN_TOKEN: string | null = getConfig().adminToken;
export const ENABLE_HANDOFF_TARGET = getConfig().enableHandoffTarget;
export const ENABLE_STOP_REASON = getConfig().enableStopReason;
diff --git a/vscode-agentchatbus/package-lock.json b/vscode-agentchatbus/package-lock.json
index 55aeb3a..08b0b9b 100644
--- a/vscode-agentchatbus/package-lock.json
+++ b/vscode-agentchatbus/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "agentchatbus",
- "version": "0.2.30",
+ "version": "0.2.31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agentchatbus",
- "version": "0.2.30",
+ "version": "0.2.31",
"dependencies": {
"eventsource": "^2.0.2"
},
diff --git a/vscode-agentchatbus/package.json b/vscode-agentchatbus/package.json
index bb0135e..115e6be 100644
--- a/vscode-agentchatbus/package.json
+++ b/vscode-agentchatbus/package.json
@@ -7,7 +7,7 @@
"name": "agentchatbus",
"displayName": "AgentChatBus",
"description": "AgentChatBus client for VS Code",
- "version": "0.2.30",
+ "version": "0.2.31",
"icon": "resources/bus_128.png",
"engines": {
"vscode": "^1.105.0"
diff --git a/vscode-agentchatbus/resources/web-ui/css/main.css b/vscode-agentchatbus/resources/web-ui/css/main.css
index 885410c..000f68f 100644
--- a/vscode-agentchatbus/resources/web-ui/css/main.css
+++ b/vscode-agentchatbus/resources/web-ui/css/main.css
@@ -799,6 +799,124 @@ body[data-theme="light"] .thread-item .ti-pin-btn {
min-height: 0;
}
+#main-panels {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+}
+
+#main.compare-mode #main-panels {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: 0;
+}
+
+#primary-panel {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+}
+
+.compare-banner {
+ flex-shrink: 0;
+ padding: 8px 16px;
+ font-size: 13px;
+ text-align: center;
+ color: var(--text-2);
+ background: var(--surface-2);
+ border-bottom: 1px solid var(--border);
+}
+
+.compare-panel {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ border-left: 1px solid var(--border);
+ background: var(--surface-1);
+}
+
+.compare-panel-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 0 16px;
+ height: 52px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.compare-panel-title {
+ flex: 1;
+ min-width: 0;
+ font-size: 14px;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.compare-panel-close {
+ flex-shrink: 0;
+ padding: 6px 12px;
+ font-size: 12px;
+ border: 1px solid var(--border-light);
+ border-radius: 8px;
+ background: transparent;
+ color: var(--text-1);
+ cursor: pointer;
+}
+
+.compare-panel-close:hover {
+ background: var(--surface-2);
+}
+
+.compare-messages-wrap {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ min-width: 0;
+}
+
+#compare-sys-prompt-area {
+ flex-shrink: 0;
+}
+
+.compare-chat-body {
+ flex: 1;
+ min-height: 0;
+ min-width: 0;
+ display: flex;
+}
+
+.compare-messages-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 16px;
+}
+
+#compare-messages {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+@media (max-width: 900px) {
+ #main.compare-mode #main-panels {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #main.compare-mode .compare-panel {
+ display: none !important;
+ }
+}
+
/* Thread header */
#thread-header {
padding: 0 20px;
diff --git a/vscode-agentchatbus/resources/web-ui/index.html b/vscode-agentchatbus/resources/web-ui/index.html
index 3df4c6f..6bcf36f 100644
--- a/vscode-agentchatbus/resources/web-ui/index.html
+++ b/vscode-agentchatbus/resources/web-ui/index.html
@@ -10,7 +10,7 @@
-
+
@@ -88,6 +88,10 @@
@@ -179,7 +200,7 @@
-
+
@@ -190,18 +211,19 @@
-
+
-
+
-
+
+
-
+
@@ -377,19 +399,7 @@
if (window.AcbSearch) window.AcbSearch.init();
await syncShowAdPolicy();
await refreshThreadSidebarData();
- const storedActiveThread = loadStoredActiveThread();
- if (storedActiveThread?.id) {
- try {
- await selectThread(
- storedActiveThread.id,
- storedActiveThread.topic || storedActiveThread.id,
- storedActiveThread.status || 'discuss',
- );
- } catch {
- clearStoredActiveThread();
- resetThreadSelection();
- }
- }
+ await restoreBootThreadSelection();
updateOnlinePresence();
startSSE();
setInterval(refreshThreadSidebarData, 10000);
@@ -425,8 +435,29 @@
hideAgentTooltip();
});
document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') hideThreadContextMenu();
+ if (e.key !== 'Escape') {
+ return;
+ }
+ const menu = document.getElementById('thread-context-menu');
+ if (menu?.classList.contains('visible')) {
+ hideThreadContextMenu();
+ return;
+ }
+ if (window.AcbSplitView?.isCompareMode()) {
+ exitCompare();
+ }
});
+ window.addEventListener('hashchange', () => {
+ void handleThreadHashChange();
+ });
+
+ const comparePanelCloseBtn = document.getElementById('compare-panel-close');
+ if (comparePanelCloseBtn) {
+ comparePanelCloseBtn.addEventListener('click', () => {
+ exitCompare();
+ });
+ }
+
window.addEventListener('resize', () => {
hideThreadContextMenu();
hideAgentTooltip();
@@ -1035,9 +1066,13 @@
function startSSE() {
return window.AcbSSE.startSSE({
getActiveThreadId: () => activeThreadId,
+ getCompareThreadId: () => window.AcbSplitView?.getCompareThreadId() || null,
onMsgNew: async () => loadNewMessages(),
onTranscriptUpdate: async () => reloadTranscript(),
onMsgEdit: async (message) => applyEditedMessage(message),
+ onCompareMsgNew: async () => loadCompareNewMessages(),
+ onCompareTranscriptUpdate: async () => reloadCompareTranscript(),
+ onCompareMsgEdit: async (message) => applyCompareEditedMessage(message),
onThreadEvent: async () => refreshThreads(),
onAgentPresence: async () => refreshThreadSidebarData(),
onCliSessionEvent: async (event) => {
@@ -1093,6 +1128,8 @@
},
resetThreadSelection,
onSelectThread: selectThread,
+ onCompareWithCurrent,
+ onThreadsRefreshed: (threads) => ensureCompareThreadStillExists(threads),
onTogglePin: async (threadId, pinned) => {
window.AcbThreads.setThreadPinned(threadId, pinned);
await refreshThreads();
@@ -1188,7 +1225,16 @@
async function selectThread(id, topic, status, initialSyncContext = null) {
// Close mobile sidebar when selecting thread
closeMobileSidebar();
+ if (window.AcbSplitView?.getCompareThreadId() === id) {
+ await exitCompare();
+ }
saveStoredActiveThread({ id, topic, status });
+ if (window.AcbSplitView) {
+ window.AcbSplitView.writeThreadHash({
+ threadId: id,
+ compareId: window.AcbSplitView.getCompareThreadId() || null,
+ });
+ }
if (window.AcbCliSessions) {
window.AcbCliSessions.restoreThreadFromCache?.(id);
}
@@ -1225,6 +1271,230 @@
// Expose globally for AcbSearch navigation
window.selectThread = selectThread;
+ function compareScrollBottom(smooth = false) {
+ const box = document.getElementById('compare-messages-scroll');
+ if (!box) return;
+ setTimeout(() => {
+ void box.offsetHeight;
+ if (smooth) {
+ box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });
+ } else {
+ box.scrollTop = box.scrollHeight;
+ }
+ }, 50);
+ }
+
+ async function loadCompareTranscript(compareId) {
+ const targets = window.AcbSplitView.getMessageTargets(window.AcbSplitView.TARGET_COMPARE);
+ return window.AcbChat.loadTranscript({
+ id: compareId,
+ api,
+ clearThreadParticipants: () => {},
+ rebuildActiveThreadParticipants: () => {},
+ appendBubble: (m) => appendBubble(m, {
+ target: window.AcbSplitView.TARGET_COMPARE,
+ readOnly: true,
+ }),
+ updateOnlinePresence: () => {},
+ updateStatusBar: async () => {},
+ setLastSeq: (s) => window.AcbSplitView.setCompareLastSeq(s),
+ scrollBottom: compareScrollBottom,
+ refreshAdmin: false,
+ clearCliCache: false,
+ messagesEl: targets.messagesEl,
+ sysPromptEl: targets.sysPromptEl,
+ skipCliActivity: true,
+ });
+ }
+
+ async function loadCompareNewMessages() {
+ const compareId = window.AcbSplitView.getCompareThreadId();
+ if (!compareId) return;
+ const targets = window.AcbSplitView.getMessageTargets(window.AcbSplitView.TARGET_COMPARE);
+ return window.AcbChat.loadNewMessagesForThread({
+ threadId: compareId,
+ getLastSeq: () => window.AcbSplitView.getCompareLastSeq(),
+ api,
+ appendBubble: (m) => appendBubble(m, {
+ target: window.AcbSplitView.TARGET_COMPARE,
+ readOnly: true,
+ }),
+ setLastSeq: (fn) => window.AcbSplitView.setCompareLastSeq(fn),
+ scrollBottom: compareScrollBottom,
+ messagesEl: targets.messagesEl,
+ skipPresence: true,
+ });
+ }
+
+ async function reloadCompareTranscript() {
+ const compareId = window.AcbSplitView.getCompareThreadId();
+ if (!compareId) return;
+ const targets = window.AcbSplitView.getMessageTargets(window.AcbSplitView.TARGET_COMPARE);
+ return window.AcbChat.reloadTranscriptForThread({
+ threadId: compareId,
+ api,
+ clearThreadParticipants: () => {},
+ rebuildActiveThreadParticipants: () => {},
+ appendBubble: (m) => appendBubble(m, {
+ target: window.AcbSplitView.TARGET_COMPARE,
+ readOnly: true,
+ }),
+ updateOnlinePresence: () => {},
+ updateStatusBar: async () => {},
+ setLastSeq: (s) => window.AcbSplitView.setCompareLastSeq(s),
+ scrollBottom: compareScrollBottom,
+ messagesEl: targets.messagesEl,
+ sysPromptEl: targets.sysPromptEl,
+ skipCliActivity: true,
+ });
+ }
+
+ async function enterCompare(compareId, topic = '') {
+ if (!window.AcbSplitView || !activeThreadId) {
+ return false;
+ }
+ return window.AcbSplitView.enterCompare(compareId, {
+ activeThreadId,
+ topic,
+ onLoadTranscript: loadCompareTranscript,
+ });
+ }
+
+ async function exitCompare() {
+ if (!window.AcbSplitView) {
+ return false;
+ }
+ return window.AcbSplitView.exitCompare({ activeThreadId });
+ }
+
+ async function onCompareWithCurrent(compareId, topic, status) {
+ void status;
+ if (!window.AcbSplitView?.isCompareViewportOk()) {
+ window.AcbSplitView.showCompareBlockedBanner();
+ return;
+ }
+ await enterCompare(compareId, topic);
+ }
+
+ async function compareThreadFromMenu() {
+ if (!contextMenuThread?.id) return;
+ hideThreadContextMenu();
+ await onCompareWithCurrent(contextMenuThread.id, contextMenuThread.topic, contextMenuThread.status);
+ }
+ window.compareThreadFromMenu = compareThreadFromMenu;
+
+ async function resolveThreadFromList(threadId) {
+ const normalizedId = String(threadId || '').trim();
+ if (!normalizedId) return null;
+ const cached = window.AcbThreads.getCachedAllThreads().find((thread) => thread.id === normalizedId);
+ if (cached) return cached;
+ try {
+ const response = await api('/api/threads?include_archived=1&limit=500');
+ return (response?.threads || []).find((thread) => thread.id === normalizedId) || null;
+ } catch {
+ return null;
+ }
+ }
+
+ async function restoreBootThreadSelection() {
+ if (!window.AcbSplitView) return;
+ const { threadId, compareId } = window.AcbSplitView.parseThreadHash();
+ let bootedPrimary = false;
+
+ if (threadId) {
+ const thread = await resolveThreadFromList(threadId);
+ if (thread) {
+ try {
+ await selectThread(thread.id, thread.topic, thread.status);
+ bootedPrimary = true;
+ } catch {
+ window.AcbSplitView.writeThreadHash({ threadId: null, compareId: null });
+ }
+ } else {
+ window.AcbSplitView.writeThreadHash({ threadId: null, compareId: compareId || null });
+ }
+ }
+
+ if (!bootedPrimary) {
+ const storedActiveThread = loadStoredActiveThread();
+ if (storedActiveThread?.id) {
+ try {
+ await selectThread(
+ storedActiveThread.id,
+ storedActiveThread.topic || storedActiveThread.id,
+ storedActiveThread.status || 'discuss',
+ );
+ bootedPrimary = true;
+ } catch {
+ clearStoredActiveThread();
+ resetThreadSelection();
+ }
+ }
+ }
+
+ if (!bootedPrimary) {
+ return;
+ }
+
+ const hashCompareId = String(compareId || '').trim();
+ if (hashCompareId && hashCompareId !== activeThreadId && window.AcbSplitView.isCompareViewportOk()) {
+ const compareThread = await resolveThreadFromList(hashCompareId);
+ if (compareThread) {
+ await enterCompare(compareThread.id, compareThread.topic);
+ return;
+ }
+ }
+
+ if (!hashCompareId) {
+ const storedCompare = window.AcbSplitView.loadCompareThread();
+ if (storedCompare?.id && storedCompare.id !== activeThreadId && window.AcbSplitView.isCompareViewportOk()) {
+ const compareThread = await resolveThreadFromList(storedCompare.id);
+ if (compareThread) {
+ await enterCompare(compareThread.id, compareThread.topic || storedCompare.topic);
+ }
+ }
+ }
+ }
+
+ async function handleThreadHashChange() {
+ if (!window.AcbSplitView) return;
+ const { threadId, compareId } = window.AcbSplitView.parseThreadHash();
+
+ if (threadId && threadId !== activeThreadId) {
+ const thread = await resolveThreadFromList(threadId);
+ if (thread) {
+ await selectThread(thread.id, thread.topic, thread.status);
+ }
+ }
+
+ const normalizedCompareId = String(compareId || '').trim();
+ if (!normalizedCompareId) {
+ if (window.AcbSplitView.isCompareMode()) {
+ await exitCompare();
+ }
+ return;
+ }
+
+ if (
+ normalizedCompareId !== window.AcbSplitView.getCompareThreadId() &&
+ normalizedCompareId !== activeThreadId
+ ) {
+ const compareThread = await resolveThreadFromList(normalizedCompareId);
+ if (compareThread) {
+ await enterCompare(compareThread.id, compareThread.topic);
+ }
+ }
+ }
+
+ function ensureCompareThreadStillExists(threads) {
+ const compareId = window.AcbSplitView?.getCompareThreadId();
+ if (!compareId) return;
+ const list = Array.isArray(threads) ? threads : [];
+ if (!list.some((thread) => thread.id === compareId)) {
+ void exitCompare();
+ }
+ }
+
function getThreadSyncContext(threadId = activeThreadId) {
if (!threadId) return null;
return threadSyncContexts.get(threadId) || null;
@@ -1290,7 +1560,10 @@
}
function openThreadContextMenu(event, thread) {
- contextMenuThread = window.AcbThreads.openThreadContextMenu(event, thread, { showAd });
+ contextMenuThread = window.AcbThreads.openThreadContextMenu(event, thread, {
+ showAd,
+ activeThreadId,
+ });
// 保存右键点击的位置,用于确认对话框定位
contextMenuThread._clickX = event.clientX;
contextMenuThread._clickY = event.clientY;
@@ -1418,6 +1691,9 @@
}
function resetThreadSelection() {
+ if (window.AcbSplitView?.isCompareMode()) {
+ void exitCompare();
+ }
if (activeThreadId) {
threadSyncContexts.delete(activeThreadId);
}
@@ -1432,9 +1708,16 @@
document.querySelectorAll('.thread-item').forEach(el => el.classList.remove('active'));
document.getElementById('thread-header').style.display = 'none';
document.getElementById('compose').classList.remove('visible');
- document.getElementById('messages').innerHTML = EMPTY_STATE_TEMPLATE;
- const sysArea = document.getElementById('sys-prompt-area');
+ const primaryTargets = window.AcbSplitView?.getMessageTargets(window.AcbSplitView?.TARGET_PRIMARY);
+ const messagesEl = primaryTargets?.messagesEl || document.getElementById('messages');
+ if (messagesEl) {
+ messagesEl.innerHTML = EMPTY_STATE_TEMPLATE;
+ }
+ const sysArea = primaryTargets?.sysPromptEl || document.getElementById('sys-prompt-area');
if (sysArea) sysArea.innerHTML = '';
+ if (window.AcbSplitView) {
+ window.AcbSplitView.writeThreadHash({ threadId: null, compareId: null });
+ }
if (window.AcbCliSessions) window.AcbCliSessions.renderThread(null);
updateOnlinePresence();
updateCloseThreadButton();
@@ -1617,6 +1900,24 @@
setTimeout(() => target.classList.remove('msg-jump-highlight'), 1500);
};
+ function applyCompareEditedMessage(message) {
+ const compareId = window.AcbSplitView?.getCompareThreadId();
+ if (!message?.id || !compareId || message.thread_id !== compareId) {
+ return;
+ }
+ const targets = window.AcbSplitView.getMessageTargets(window.AcbSplitView.TARGET_COMPARE);
+ const root = targets.messagesEl;
+ if (!root) return;
+ const row = root.querySelector(`[data-msg-id="${CSS.escape(String(message.id))}"]`);
+ if (!row || row.classList.contains('msg-editing')) {
+ return;
+ }
+ const bubbleEl = row.querySelector('.bubble-v2');
+ if (!bubbleEl) return;
+ renderMessageBubble(row, bubbleEl, message.content, message.metadata);
+ syncEditedIndicator(row, message);
+ }
+
// UI-13: event delegation -- intercepts clicks on any .msg-reply-quote in #messages
document.getElementById('messages')?.addEventListener('click', (e) => {
const btn = e.target.closest('button.msg-reply-quote');
@@ -1625,14 +1926,21 @@
if (targetId) window.AcbScrollToMsg(targetId);
});
- function appendBubble(m) {
- const box = document.getElementById('messages');
+ function appendBubble(m, options = {}) {
+ const targetKey = options.target || window.AcbSplitView?.TARGET_PRIMARY || 'primary';
+ const readOnly = Boolean(options.readOnly);
+ const targets = window.AcbSplitView?.getMessageTargets(targetKey) || {
+ messagesEl: document.getElementById('messages'),
+ sysPromptEl: document.getElementById('sys-prompt-area'),
+ };
+ const box = targets.messagesEl;
+ if (!box) return;
// ---- Deduplication check (also check sys-prompt-area for seq=0) --------
const messageId = String(m.id ?? '').trim();
const hasSeq = typeof m.seq === 'number' && Number.isFinite(m.seq);
const seq = hasSeq ? String(m.seq) : '';
- const sysPromptArea = document.getElementById('sys-prompt-area');
+ const sysPromptArea = targets.sysPromptEl;
const escapedMessageId = messageId && window.CSS && typeof window.CSS.escape === 'function'
? window.CSS.escape(messageId)
: messageId;
@@ -1655,7 +1963,7 @@
// Frontend fallback: still render legacy human-decision admin switch cards
// if old messages exist in history.
- if (metaObj && (
+ if (!readOnly && metaObj && (
metaObj.ui_type === 'admin_switch_confirmation_required'
|| metaObj.ui_type === 'admin_takeover_confirmation_required'
)) {
@@ -1772,7 +2080,7 @@
promptBody.appendChild(promptCopyBtn);
// UI-07: system prompt goes into its own full-width area (above chat-body)
- const sysArea = document.getElementById('sys-prompt-area') || box;
+ const sysArea = targets.sysPromptEl || box;
sysArea.innerHTML = '';
sysArea.appendChild(wrapper);
return;
@@ -1892,7 +2200,7 @@
- ${!isSystem ? `