优化项目对话、刷新续流、实时滚动与工具状态恢复 (#245)

* feat(chat): add project-based conversation sidebar

* feat(chat): refine Codex-style conversation UI

* feat(chat): add Codex-style conversation workflow

* feat(ui): 优化对话框与项目侧边栏交互

* fix(chat): 修复暗色输入框圆角填色

* fix(chat): 恢复输入区分层错位布局

* fix(hitl): isolate reviewer state per conversation

* feat(hitl): 增加双入口审批与倒计时进度

* fix(hitl): 汇总项目审批并隔离对话状态

* fix(ui): 修复审批状态与无项目新任务

* fix(ui): 优化审批状态与对话切换性能

* fix(ui): 修复中断任务审批仍计时

* fix(ui): 修复多对话并发切换卡顿

* fix(hitl): 主动同步审批并关闭中断状态

* fix(ui): 固定项目审批汇总为绿色

* feat(ui): 同步系统模型与推理强度

* feat(ui): 完善项目侧栏预览与新建入口

* fix(ui): 防止无项目文件夹误展开

* fix(ui): 防止长历史对话滚动误触审批

* fix(ui): 修复对话操作并补充项目置顶

* fix(ui): 移除对话分组并调整项目置顶排序

* feat(ui): 优化迭代导航与审批交互

* fix(hitl): 将 write_file 加入内置免审批工具

* fix(chat): 支持回车发送与 Shift 回车换行

* fix(chat): 优化对话刷新与 Codex 风格交互

* fix(ui): 显示对话具体更新时间

* fix(ui): 优化对话刷新与项目加载

* fix(ui): 修复 Agent 审查文字裁切

* fix(chat): 修复刷新续流与多标签页同步

* fix(chat): 修复滚动跟随与中断任务终态

* fix(ui): 修复流式滚动跳动与工具状态恢复

* fix(ui): 修复刷新后流式输出停止粘底
This commit is contained in:
RuoJi6
2026-08-13 21:28:37 +08:00
committed by GitHub
parent b170f2c4b1
commit eb6bab574f
31 changed files with 10763 additions and 886 deletions
+48 -38
View File
@@ -98,6 +98,7 @@ function hitlT(key, fallback, params) {
const HITL_LOGS_PAGE_SIZE_KEY = 'cyberstrike_hitl_logs_page_size';
const HITL_PENDING_PAGE_SIZE_KEY = 'cyberstrike_hitl_pending_page_size';
const HITL_TIMEOUT_DEFAULT_MIGRATION_PREFIX = 'cyberstrike-hitl-timeout-default-v1:';
const HITL_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
function hitlPaginationT(key, opts, fallback) {
@@ -212,6 +213,21 @@ function normalizeHitlTimeoutSeconds(v, fallback) {
return 0;
}
function shouldMigrateLegacyHitlTimeout(conversationId, timeoutSeconds) {
if (!conversationId || normalizeHitlTimeoutSeconds(timeoutSeconds, 0) > 0) return false;
try {
return localStorage.getItem(HITL_TIMEOUT_DEFAULT_MIGRATION_PREFIX + conversationId) !== '1';
} catch (e) {
return false;
}
}
function markLegacyHitlTimeoutMigrated(conversationId) {
try {
localStorage.setItem(HITL_TIMEOUT_DEFAULT_MIGRATION_PREFIX + conversationId, '1');
} catch (e) { /* ignore */ }
}
function getCurrentConversationIdForHitl() {
if (typeof window.currentConversationId === 'string' && window.currentConversationId) {
return window.currentConversationId;
@@ -241,16 +257,6 @@ function applyHitlDefaultReviewerFromServer(reviewer) {
if (typeof window !== 'undefined') {
window.csaiHitlDefaultReviewer = v;
}
if (typeof window.saveHitlLastGlobalConfig === 'function' && typeof window.getHitlLastGlobalConfig === 'function') {
const gl = window.getHitlLastGlobalConfig();
const base = gl && typeof gl === 'object'
? gl
: { mode: 'off', sensitiveTools: '', updatedAt: '' };
window.saveHitlLastGlobalConfig(Object.assign({}, base, {
reviewer: v,
updatedAt: new Date().toISOString()
}));
}
return v;
}
@@ -352,9 +358,9 @@ function showHitlPageWhitelistFeedback(text, isError) {
el.className = 'hitl-apply-feedback' + (isError ? ' hitl-apply-feedback--error' : '');
}
function syncHitlSidebarWhitelistDisplay(toolsStr) {
const sidebarEl = document.getElementById('hitl-sensitive-tools');
if (sidebarEl) sidebarEl.value = toolsStr;
function syncHitlSidebarWhitelistDisplay(_toolsStr) {
// The chat field is conversation-scoped. Updating the global allowlist page
// must not replace it with a merged global + conversation display value.
}
async function fetchHitlGlobalToolWhitelist() {
@@ -534,45 +540,41 @@ async function syncHitlConfigFromServer(conversationId) {
const local = readHitlLocalStorageConv(conversationId);
const localMode = local && local.mode ? hitlModeNormalize(local.mode) : 'off';
if (localMode !== 'off') {
const localReviewer = hitlReviewerNormalize(local && local.reviewer);
let localToolsStr = typeof local.sensitiveTools === 'string' ? local.sensitiveTools : '';
localToolsStr = strip(globalWL, localToolsStr);
merged = {
enabled: true,
mode: localMode,
reviewer: localReviewer,
sensitiveTools: localToolsStr.split(/[,\n\r]+/).map(function (s) { return s.trim(); }).filter(Boolean),
timeoutSeconds: normalizeHitlTimeoutSeconds(cfg.timeoutSeconds, 0)
timeoutSeconds: normalizeHitlTimeoutSeconds(
local && local.timeoutSeconds,
normalizeHitlTimeoutSeconds(cfg.timeoutSeconds, 0)
)
};
saveHitlConversationConfig(conversationId, {
mode: localMode,
reviewer: localReviewer,
sensitiveTools: localToolsStr,
enabled: true,
timeoutSeconds: merged.timeoutSeconds
}).catch(function (err) {
console.warn('HITL 会话配置同步到服务器失败(将仅保留本地 UI):', err);
});
} else {
const gl = typeof window.getHitlLastGlobalConfig === 'function' ? window.getHitlLastGlobalConfig() : null;
const glMode = gl && gl.mode ? hitlModeNormalize(gl.mode) : 'off';
if (glMode !== 'off') {
let glToolsStr = typeof gl.sensitiveTools === 'string' ? gl.sensitiveTools : '';
glToolsStr = strip(globalWL, glToolsStr);
merged = {
enabled: true,
mode: glMode,
sensitiveTools: glToolsStr.split(/[,\n\r]+/).map(function (s) { return s.trim(); }).filter(Boolean),
timeoutSeconds: normalizeHitlTimeoutSeconds(cfg.timeoutSeconds, 0)
};
saveHitlConversationConfig(conversationId, {
mode: glMode,
sensitiveTools: glToolsStr,
enabled: true,
timeoutSeconds: merged.timeoutSeconds
}).catch(function (err) {
console.warn('HITL 会话配置同步到服务器失败(将仅保留本地 UI):', err);
});
}
}
}
if (shouldMigrateLegacyHitlTimeout(conversationId, merged.timeoutSeconds)) {
merged = Object.assign({}, merged, { timeoutSeconds: 300 });
try {
await saveHitlConversationConfig(conversationId, merged);
markLegacyHitlTimeoutMigrated(conversationId);
} catch (err) {
console.warn('HITL 旧会话等待时限迁移失败,将在下次加载时重试:', err);
}
} else if (normalizeHitlTimeoutSeconds(merged.timeoutSeconds, 0) > 0) {
markLegacyHitlTimeoutMigrated(conversationId);
}
const uiMode = hitlEffectiveEnabled(merged) ? hitlModeNormalize(merged.mode) : 'off';
const rawArr = Array.isArray(merged.sensitiveTools)
? merged.sensitiveTools
@@ -590,7 +592,10 @@ async function syncHitlConfigFromServer(conversationId) {
localStorage.setItem('chat_hitl_config_' + conversationId, JSON.stringify(normalizedCfg));
} catch (e) {}
}
if (typeof window.applyHitlConfigToUI === 'function') {
if (
getCurrentConversationIdForHitl() === conversationId &&
typeof window.applyHitlConfigToUI === 'function'
) {
window.applyHitlConfigToUI(normalizedCfg);
}
reconcileHitlUiState();
@@ -835,7 +840,11 @@ async function refreshHitlPending() {
throw new Error('request failed');
}
const data = await resp.json();
const items = Array.isArray(data.items) ? data.items : [];
const rawItems = Array.isArray(data.items) ? data.items : [];
const items = rawItems.filter(function (item) {
return hitlReviewerNormalize(item && (item.reviewer || item.decidedBy || item.decided_by)) !== 'audit_agent' &&
String(item && item.status || '').trim().toLowerCase() !== 'audit_running';
});
let workflowRuns = [];
try {
const wfResp = await hitlApiFetch('/api/workflows/runs/pending', { credentials: 'same-origin' });
@@ -856,7 +865,8 @@ async function refreshHitlPending() {
return conv.indexOf(searchQ) >= 0 || wfId.indexOf(searchQ) >= 0 || runId.indexOf(searchQ) >= 0 || label.indexOf(searchQ) >= 0;
});
}
hitlPendingTotal = (typeof data.total === 'number' ? data.total : items.length) + workflowRuns.length;
const hiddenAgentItems = rawItems.length - items.length;
hitlPendingTotal = Math.max(0, (typeof data.total === 'number' ? data.total : rawItems.length) - hiddenAgentItems) + workflowRuns.length;
const maxPage = Math.max(1, Math.ceil(hitlPendingTotal / hitlPendingPageSize));
if (hitlPendingPage > maxPage) {
hitlPendingPage = maxPage;