feat: improve cross-conversation reply UX
This commit is contained in:
43
.cbmignore
Normal file
43
.cbmignore
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# codebase-memory-mcp 专用忽略规则。
|
||||||
|
# 保留源码级 public/app.js、public/style.css 等文件;只排除生成物、压缩物和临时产物。
|
||||||
|
|
||||||
|
# 依赖、运行态数据和日志(多数已在 .gitignore,这里显式补强)
|
||||||
|
node_modules/
|
||||||
|
sessions/
|
||||||
|
logs/
|
||||||
|
attachments/
|
||||||
|
config/cross-conversation-replies.json
|
||||||
|
|
||||||
|
# 构建与覆盖率产物
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.vite/
|
||||||
|
.next/
|
||||||
|
.nuxt/
|
||||||
|
|
||||||
|
# 压缩、归档和二进制产物
|
||||||
|
*.zip
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
*.tgz
|
||||||
|
*.gz
|
||||||
|
*.7z
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# 前端生成物:保留普通源码,只排除压缩/映射/打包结果
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
*.bundle.js
|
||||||
|
*.bundle.css
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.log
|
||||||
|
* TO DO list.csv
|
||||||
8
.codex/config.toml
Normal file
8
.codex/config.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[mcp_servers.codebase-memory-mcp]
|
||||||
|
type = "stdio"
|
||||||
|
command = "/home/hdzx/.local/bin/codebase-memory-mcp"
|
||||||
|
args = []
|
||||||
|
enabled = true
|
||||||
|
startup_timeout_sec = 20
|
||||||
|
tool_timeout_sec = 120
|
||||||
|
env = { CBM_LOG_LEVEL = "info", CBM_CACHE_DIR = "/home/hdzx/.cache/codebase-memory-mcp" }
|
||||||
64
AGENTS.md
64
AGENTS.md
@@ -25,10 +25,74 @@ Keep this managed block so 'trellis update' can refresh the instructions.
|
|||||||
|
|
||||||
重启 cc-web 服务:
|
重启 cc-web 服务:
|
||||||
|
|
||||||
|
修改 ccweb 项目后如果需要重启服务,必须先查看当前会话列表中的运行状态。
|
||||||
|
只有在除当前对话外没有其他 `running` 对话时,才能执行重启;如果仍有其他
|
||||||
|
运行中的对话,应暂缓重启并告知用户原因。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pm2 restart ccweb --update-env
|
pm2 restart ccweb --update-env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Codebase Memory 代码检索约定
|
||||||
|
|
||||||
|
本项目内 **graphify 已全面弃用**。后续涉及代码定位、调用链、架构理解、影响面分析、跨模块关系梳理时,默认优先使用 `codebase-memory-mcp`,不要再启用 graphify 技能、graphify CLI 或 `graphify-out` 产物作为主路径。
|
||||||
|
|
||||||
|
当前项目的 codebase-memory 项目名:
|
||||||
|
|
||||||
|
```text
|
||||||
|
home-cc-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### 默认检索流程
|
||||||
|
|
||||||
|
1. 先确认索引状态:
|
||||||
|
- `list_projects`
|
||||||
|
- `index_status(project="home-cc-web")`
|
||||||
|
2. 如果索引缺失或明显过期,先执行:
|
||||||
|
- `index_repository(repo_path="/home/cc-web", mode="full", persistence=false)`
|
||||||
|
3. 需要理解整体结构时,先用:
|
||||||
|
- `get_architecture(project="home-cc-web", aspects=["all"])`
|
||||||
|
4. 需要找功能入口、类、函数、路由时,优先用:
|
||||||
|
- `search_graph`
|
||||||
|
- `search_code`
|
||||||
|
5. 需要确认调用关系时,使用:
|
||||||
|
- `trace_path(mode="calls", direction="inbound" | "outbound" | "both")`
|
||||||
|
6. 需要读取具体实现时,先通过 `search_graph` 找到精确 `qualified_name`,再用:
|
||||||
|
- `get_code_snippet`
|
||||||
|
7. `rg` / `sed` 只作为补充校验:
|
||||||
|
- 校验最终行号
|
||||||
|
- 查未被索引的配置或纯文本
|
||||||
|
- 对 MCP 命中结果做交叉验证
|
||||||
|
|
||||||
|
### 子代理引导词模板
|
||||||
|
|
||||||
|
派发需要理解代码的子代理时,默认加入以下引导:
|
||||||
|
|
||||||
|
```text
|
||||||
|
请优先使用 codebase-memory-mcp 做代码理解:
|
||||||
|
先 list_projects / index_status 确认项目索引;
|
||||||
|
再用 search_graph 或 search_code 定位候选函数;
|
||||||
|
需要调用链时用 trace_path;
|
||||||
|
需要源码时先拿 qualified_name,再调用 get_code_snippet;
|
||||||
|
最后只用 rg/sed 校验行号或补查未索引文本。
|
||||||
|
不要使用 graphify。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 本项目已验证的使用经验
|
||||||
|
|
||||||
|
- 无明确引导时,子代理不一定会主动使用 `codebase-memory-mcp`,可能退回 `rg` 或旧的 graphify 路径。
|
||||||
|
- 明确要求优先使用 `codebase-memory-mcp` 后,能稳定命中函数级入口、调用链和源码片段。
|
||||||
|
- 对 `codexapp` / `ccweb` 这类跨模块链路,`search_code` + `trace_path` + `get_code_snippet` 的组合比单纯全文检索更快建立上下文。
|
||||||
|
- 自然语言检索遇到 `developer`、`config`、`message` 等通用词会有噪声;这时应收敛到明确标识符,如 `collaborationMode`、`mcp_servers.ccweb`、`CC_WEB_SOURCE_SESSION_ID`。
|
||||||
|
- 最终答复中的文件行号仍建议用 `rg -n` 或 `nl -ba` 做一次轻量核验。
|
||||||
|
|
||||||
|
### 索引更新与忽略规则
|
||||||
|
|
||||||
|
- `auto_index` 是 `codebase-memory-mcp` 的本地配置,按当前 `CBM_CACHE_DIR` 生效;它不是 `.codex/config.toml` 里的项目级开关。
|
||||||
|
- 当前项目的项目级忽略规则写在 `.cbmignore`。普通源码级 `*.js` / `*.css` 不应一刀切排除;只排除 `*.min.js`、`*.map`、压缩包、构建目录、日志、临时文件、运行态状态文件等。
|
||||||
|
- watcher 是 Git-based polling:非 Git 项目跳过轮询;Git 项目的轮询间隔为基础 5 秒,每 500 个文件加 1 秒,最长 60 秒。
|
||||||
|
- 修改 `.cbmignore` 或大范围调整文件后,建议手动执行一次 `index_repository(repo_path="/home/cc-web", mode="full", persistence=false)`,让当前索引立即收敛到最新规则。
|
||||||
|
|
||||||
## Codex App / hapi 对齐经验
|
## Codex App / hapi 对齐经验
|
||||||
|
|
||||||
以下约定来自本项目对 `/home/hdzx/2026/hapi` 中 Codex app-server 接入方式的对比结果。后续如果继续维护 `codexapp`,默认按这些约定实现,避免再走偏到“看起来能跑、但拿不到原生协作能力”的分叉路径。
|
以下约定来自本项目对 `/home/hdzx/2026/hapi` 中 Codex app-server 接入方式的对比结果。后续如果继续维护 `codexapp`,默认按这些约定实现,避免再走偏到“看起来能跑、但拿不到原生协作能力”的分叉路径。
|
||||||
|
|||||||
5
config/cross-conversation-replies.json
Normal file
5
config/cross-conversation-replies.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"updatedAt": "2026-06-21T15:06:34.507Z",
|
||||||
|
"replies": []
|
||||||
|
}
|
||||||
426
public/app.js
426
public/app.js
@@ -2,12 +2,42 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const ASSET_VERSION = '20260618-mobile-session-switch';
|
const ASSET_VERSION = '20260621-cross-reply-collapse-last-section-offset';
|
||||||
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
|
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
|
||||||
const RENDER_DEBOUNCE = 100;
|
const RENDER_DEBOUNCE = 100;
|
||||||
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
|
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
|
||||||
const DIVIDER_TIME_STORAGE_KEY = 'cc-web-show-divider-time';
|
const DIVIDER_TIME_STORAGE_KEY = 'cc-web-show-divider-time';
|
||||||
const PROJECT_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-projects';
|
const PROJECT_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-projects';
|
||||||
|
const CROSS_CONVERSATION_REPLY_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-cross-replies';
|
||||||
|
const CROSS_CONVERSATION_REPLY_COLLAPSE_LIMIT = 500;
|
||||||
|
const ASSISTANT_LAST_SECTION_BUTTON_CLASS = 'msg-last-section-btn';
|
||||||
|
const ASSISTANT_LAST_SECTION_FOCUS_CLASS = 'msg-last-section-focus';
|
||||||
|
const ASSISTANT_LAST_SECTION_SCROLL_OFFSET = 72;
|
||||||
|
const ASSISTANT_LAST_SECTION_SKIP_SELECTOR = [
|
||||||
|
`.${ASSISTANT_LAST_SECTION_BUTTON_CLASS}`,
|
||||||
|
'.msg-tools',
|
||||||
|
'.tool-call',
|
||||||
|
'.tool-group',
|
||||||
|
'.msg-attachments',
|
||||||
|
'.msg-attachment-card',
|
||||||
|
'.cross-conversation-meta',
|
||||||
|
'.agent-message-divider',
|
||||||
|
].join(',');
|
||||||
|
const ASSISTANT_LAST_SECTION_SCOPE_SELECTOR = [
|
||||||
|
'p',
|
||||||
|
'li',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'td',
|
||||||
|
'th',
|
||||||
|
'pre',
|
||||||
|
'code',
|
||||||
|
'.msg-text',
|
||||||
|
].join(',');
|
||||||
|
|
||||||
const SLASH_COMMANDS = [
|
const SLASH_COMMANDS = [
|
||||||
{ cmd: '/clear', desc: '清除当前会话' },
|
{ cmd: '/clear', desc: '清除当前会话' },
|
||||||
@@ -178,6 +208,14 @@
|
|||||||
return new Set();
|
return new Set();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
const collapsedCrossConversationReplyKeys = (() => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(localStorage.getItem(CROSS_CONVERSATION_REPLY_COLLAPSE_STORAGE_KEY) || '[]');
|
||||||
|
return new Set(Array.isArray(parsed) ? parsed.filter(Boolean) : []);
|
||||||
|
} catch {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
})();
|
||||||
const pendingNotesByTarget = new Map();
|
const pendingNotesByTarget = new Map();
|
||||||
const userMessageIndex = new Map();
|
const userMessageIndex = new Map();
|
||||||
const expandedOldSessionAgents = new Set();
|
const expandedOldSessionAgents = new Set();
|
||||||
@@ -815,6 +853,51 @@
|
|||||||
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCrossConversationReplyCollapseKey(meta = {}) {
|
||||||
|
const source = meta?.crossConversation || {};
|
||||||
|
const messageId = source.replyToRequestId
|
||||||
|
|| source.messageId
|
||||||
|
|| meta.messageId
|
||||||
|
|| meta.id
|
||||||
|
|| [source.sourceSessionId, meta.timestamp || source.processedAt || source.sentAt].filter(Boolean).join(':');
|
||||||
|
if (!messageId) return '';
|
||||||
|
return `${currentSessionId || 'unknown'}:${messageId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCrossConversationReplyCollapseState() {
|
||||||
|
try {
|
||||||
|
const keys = Array.from(collapsedCrossConversationReplyKeys).slice(-CROSS_CONVERSATION_REPLY_COLLAPSE_LIMIT);
|
||||||
|
localStorage.setItem(CROSS_CONVERSATION_REPLY_COLLAPSE_STORAGE_KEY, JSON.stringify(keys));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCrossConversationReplyCollapsed(key, collapsed) {
|
||||||
|
if (!key) return;
|
||||||
|
if (collapsed) {
|
||||||
|
collapsedCrossConversationReplyKeys.delete(key);
|
||||||
|
collapsedCrossConversationReplyKeys.add(key);
|
||||||
|
} else {
|
||||||
|
collapsedCrossConversationReplyKeys.delete(key);
|
||||||
|
}
|
||||||
|
persistCrossConversationReplyCollapseState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCrossConversationReplyCollapsed(key) {
|
||||||
|
return !!key && collapsedCrossConversationReplyKeys.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCrossConversationReplyTime(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createLocalId(prefix = 'local') {
|
function createLocalId(prefix = 'local') {
|
||||||
if (window.crypto?.randomUUID) return `${prefix}-${window.crypto.randomUUID()}`;
|
if (window.crypto?.randomUUID) return `${prefix}-${window.crypto.randomUUID()}`;
|
||||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
@@ -893,6 +976,166 @@
|
|||||||
messagesDiv.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
|
messagesDiv.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAssistantLastSectionTextNode(node, root) {
|
||||||
|
if (!node || node.nodeType !== Node.TEXT_NODE || !node.nodeValue?.trim()) return false;
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (!parent || !root.contains(parent)) return false;
|
||||||
|
if (parent.closest(ASSISTANT_LAST_SECTION_SKIP_SELECTOR)) return false;
|
||||||
|
const tag = parent.tagName?.toLowerCase();
|
||||||
|
return !['button', 'script', 'style', 'textarea', 'input', 'select', 'option'].includes(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAssistantTextNodes(root) {
|
||||||
|
const nodes = [];
|
||||||
|
if (!root) return nodes;
|
||||||
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode(node) {
|
||||||
|
return isAssistantLastSectionTextNode(node, root)
|
||||||
|
? NodeFilter.FILTER_ACCEPT
|
||||||
|
: NodeFilter.FILTER_REJECT;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let node = walker.nextNode();
|
||||||
|
while (node) {
|
||||||
|
nodes.push(node);
|
||||||
|
node = walker.nextNode();
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLastAssistantTextScope(bubble) {
|
||||||
|
const nodes = collectAssistantTextNodes(bubble);
|
||||||
|
const lastNode = nodes[nodes.length - 1];
|
||||||
|
if (!lastNode) return null;
|
||||||
|
return lastNode.parentElement?.closest(ASSISTANT_LAST_SECTION_SCOPE_SELECTOR) || bubble;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAssistantTextScopes(root) {
|
||||||
|
if (!root) return [];
|
||||||
|
const seen = new Set();
|
||||||
|
return Array.from(root.querySelectorAll(ASSISTANT_LAST_SECTION_SCOPE_SELECTOR)).filter((scope) => {
|
||||||
|
if (!scope || seen.has(scope)) return false;
|
||||||
|
seen.add(scope);
|
||||||
|
if (scope.closest(ASSISTANT_LAST_SECTION_SKIP_SELECTOR)) return false;
|
||||||
|
return collectAssistantTextNodes(scope).length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFirstAssistantTextScopeAfterDivider(bubble) {
|
||||||
|
const dividers = Array.from(bubble?.querySelectorAll?.('.agent-message-divider') || []);
|
||||||
|
const lastDivider = dividers[dividers.length - 1];
|
||||||
|
if (!lastDivider) return null;
|
||||||
|
return collectAssistantTextScopes(bubble).find((scope) => (
|
||||||
|
lastDivider.compareDocumentPosition(scope) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||||
|
)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFirstNonWhitespaceIndex(text, start = 0) {
|
||||||
|
for (let i = Math.max(0, start); i < text.length; i += 1) {
|
||||||
|
if (!/\s/.test(text[i])) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTextIndexToNode(entries, index) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (index >= entry.start && index < entry.end) {
|
||||||
|
return { node: entry.node, offset: index - entry.start };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const last = entries[entries.length - 1];
|
||||||
|
return last ? { node: last.node, offset: last.node.nodeValue.length } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssistantTextScopeStartTarget(scope) {
|
||||||
|
if (!scope) return null;
|
||||||
|
const nodes = collectAssistantTextNodes(scope);
|
||||||
|
if (nodes.length === 0) return null;
|
||||||
|
const entries = [];
|
||||||
|
let text = '';
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const value = node.nodeValue || '';
|
||||||
|
const start = text.length;
|
||||||
|
text += value;
|
||||||
|
entries.push({ node, start, end: text.length });
|
||||||
|
});
|
||||||
|
const startIndex = findFirstNonWhitespaceIndex(text, 0);
|
||||||
|
if (startIndex < 0) return null;
|
||||||
|
const mapped = mapTextIndexToNode(entries, startIndex);
|
||||||
|
return mapped ? { ...mapped, scope } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssistantLastSectionTarget(bubble) {
|
||||||
|
const scope = findFirstAssistantTextScopeAfterDivider(bubble) || findLastAssistantTextScope(bubble);
|
||||||
|
return getAssistantTextScopeStartTarget(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRangeRectFromTextPosition(node, offset) {
|
||||||
|
if (!node) return null;
|
||||||
|
const range = document.createRange();
|
||||||
|
const safeOffset = Math.min(Math.max(0, offset), node.nodeValue.length);
|
||||||
|
range.setStart(node, safeOffset);
|
||||||
|
range.setEnd(node, Math.min(node.nodeValue.length, safeOffset + 1));
|
||||||
|
const rect = Array.from(range.getClientRects()).find(item => item.width || item.height) || null;
|
||||||
|
range.detach?.();
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollAssistantBubbleToLastSection(bubble) {
|
||||||
|
const target = getAssistantLastSectionTarget(bubble);
|
||||||
|
if (!target) return false;
|
||||||
|
const rect = getRangeRectFromTextPosition(target.node, target.offset) || target.scope.getBoundingClientRect();
|
||||||
|
if (!rect) return false;
|
||||||
|
const containerRect = messagesDiv.getBoundingClientRect();
|
||||||
|
const targetTop = messagesDiv.scrollTop + rect.top - containerRect.top - ASSISTANT_LAST_SECTION_SCROLL_OFFSET;
|
||||||
|
messagesDiv.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
|
||||||
|
target.scope.classList.remove(ASSISTANT_LAST_SECTION_FOCUS_CLASS);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
target.scope.classList.add(ASSISTANT_LAST_SECTION_FOCUS_CLASS);
|
||||||
|
window.setTimeout(() => target.scope.classList.remove(ASSISTANT_LAST_SECTION_FOCUS_CLASS), 1100);
|
||||||
|
});
|
||||||
|
updateScrollbar();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAssistantLastSectionButton() {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = ASSISTANT_LAST_SECTION_BUTTON_CLASS;
|
||||||
|
button.title = '定位到本条回复最后一段';
|
||||||
|
button.setAttribute('aria-label', '定位到本条回复最后一段');
|
||||||
|
button.innerHTML = `
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="7"></circle>
|
||||||
|
<circle cx="12" cy="12" r="2"></circle>
|
||||||
|
<path d="M12 2v3"></path>
|
||||||
|
<path d="M12 19v3"></path>
|
||||||
|
<path d="M2 12h3"></path>
|
||||||
|
<path d="M19 12h3"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
button.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const bubble = button.closest('.msg-bubble');
|
||||||
|
scrollAssistantBubbleToLastSection(bubble);
|
||||||
|
});
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAssistantLastSectionButton(messageEl) {
|
||||||
|
if (!messageEl?.classList?.contains('assistant')) return;
|
||||||
|
const bubble = messageEl.querySelector(':scope > .msg-bubble');
|
||||||
|
if (!bubble) return;
|
||||||
|
let button = bubble.querySelector(`:scope > .${ASSISTANT_LAST_SECTION_BUTTON_CLASS}`);
|
||||||
|
const hasTarget = !!getAssistantLastSectionTarget(bubble);
|
||||||
|
if (!button && !hasTarget) return;
|
||||||
|
if (!button) button = createAssistantLastSectionButton();
|
||||||
|
button.hidden = !hasTarget;
|
||||||
|
button.disabled = !hasTarget;
|
||||||
|
bubble.appendChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
function updateSessionIdBadge() {
|
function updateSessionIdBadge() {
|
||||||
if (!chatSessionIdBtn) return;
|
if (!chatSessionIdBtn) return;
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
@@ -907,24 +1150,70 @@
|
|||||||
chatSessionIdBtn.setAttribute('aria-label', `复制当前会话 ID ${currentSessionId}`);
|
chatSessionIdBtn.setAttribute('aria-label', `复制当前会话 ID ${currentSessionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldPreferTextareaCopy() {
|
||||||
|
const ua = navigator.userAgent || '';
|
||||||
|
return /iPad|iPhone|iPod/.test(ua)
|
||||||
|
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyTextWithTextarea(value) {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = value;
|
||||||
|
textarea.setAttribute('readonly', '');
|
||||||
|
textarea.setAttribute('aria-hidden', 'true');
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.top = '0';
|
||||||
|
textarea.style.left = '0';
|
||||||
|
textarea.style.width = '1px';
|
||||||
|
textarea.style.height = '1px';
|
||||||
|
textarea.style.padding = '0';
|
||||||
|
textarea.style.border = '0';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
textarea.style.pointerEvents = 'none';
|
||||||
|
textarea.style.fontSize = '16px';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
textarea.focus({ preventScroll: true });
|
||||||
|
} catch {
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
textarea.select();
|
||||||
|
try {
|
||||||
|
textarea.setSelectionRange(0, value.length);
|
||||||
|
} catch {}
|
||||||
|
if (!document.execCommand('copy')) throw new Error('copy_failed');
|
||||||
|
} finally {
|
||||||
|
textarea.remove();
|
||||||
|
if (activeElement && typeof activeElement.focus === 'function') {
|
||||||
|
try {
|
||||||
|
activeElement.focus({ preventScroll: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyTextToClipboard(text, successText = '已复制') {
|
async function copyTextToClipboard(text, successText = '已复制') {
|
||||||
const value = String(text || '');
|
const value = String(text || '');
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
try {
|
try {
|
||||||
if (navigator.clipboard?.writeText && window.isSecureContext) {
|
let copied = false;
|
||||||
await navigator.clipboard.writeText(value);
|
const preferTextarea = shouldPreferTextareaCopy();
|
||||||
} else {
|
if (preferTextarea) {
|
||||||
const textarea = document.createElement('textarea');
|
try {
|
||||||
textarea.value = value;
|
copyTextWithTextarea(value);
|
||||||
textarea.setAttribute('readonly', '');
|
copied = true;
|
||||||
textarea.style.position = 'fixed';
|
} catch {}
|
||||||
textarea.style.left = '-9999px';
|
|
||||||
textarea.style.top = '0';
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
textarea.remove();
|
|
||||||
}
|
}
|
||||||
|
if (!copied && navigator.clipboard?.writeText && window.isSecureContext) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
copied = true;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (!copied) copyTextWithTextarea(value);
|
||||||
showToast(successText);
|
showToast(successText);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -3008,7 +3297,7 @@
|
|||||||
}
|
}
|
||||||
const canPreview = PREVIEW_LANGS.has(lang);
|
const canPreview = PREVIEW_LANGS.has(lang);
|
||||||
const previewBtn = canPreview
|
const previewBtn = canPreview
|
||||||
? `<button class="code-preview-btn" onclick="ccTogglePreview(this)">Preview</button>`
|
? `<button class="code-preview-btn" type="button" onclick="ccTogglePreview(this, event)" aria-label="预览代码块">Preview</button>`
|
||||||
: '';
|
: '';
|
||||||
const previewPane = canPreview
|
const previewPane = canPreview
|
||||||
? `<div class="code-preview-pane"><iframe class="code-preview-iframe" sandbox="allow-scripts" loading="lazy"></iframe></div>`
|
? `<div class="code-preview-pane"><iframe class="code-preview-iframe" sandbox="allow-scripts" loading="lazy"></iframe></div>`
|
||||||
@@ -3018,24 +3307,37 @@
|
|||||||
return `<div class="code-block-wrapper${canPreview ? ' has-preview' : ''}"${canPreview ? ` data-cid="${cid}"` : ''}>
|
return `<div class="code-block-wrapper${canPreview ? ' has-preview' : ''}"${canPreview ? ` data-cid="${cid}"` : ''}>
|
||||||
<div class="code-block-header">
|
<div class="code-block-header">
|
||||||
<span>${escapeHtml(lang)}</span>
|
<span>${escapeHtml(lang)}</span>
|
||||||
<div class="code-block-actions">${previewBtn}<button class="code-copy-btn" onclick="ccCopyCode(this)">Copy</button></div>
|
<div class="code-block-actions">${previewBtn}<button class="code-copy-btn" type="button" onclick="ccCopyCode(this, event)" aria-label="复制代码块">Copy</button></div>
|
||||||
</div>
|
</div>
|
||||||
${previewPane}<pre><code class="hljs language-${escapeHtml(lang)}">${highlighted}</code></pre>
|
${previewPane}<pre><code class="hljs language-${escapeHtml(lang)}">${highlighted}</code></pre>
|
||||||
</div>`;
|
</div>`;
|
||||||
};
|
};
|
||||||
marked.setOptions({ renderer, breaks: true, gfm: true });
|
marked.setOptions({ renderer, breaks: true, gfm: true });
|
||||||
|
|
||||||
window.ccCopyCode = function (btn) {
|
window.ccCopyCode = async function (btn, event) {
|
||||||
const wrapper = btn.closest('.code-block-wrapper');
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
const wrapper = btn?.closest?.('.code-block-wrapper');
|
||||||
|
if (!wrapper) return;
|
||||||
const cid = wrapper.dataset.cid ? Number(wrapper.dataset.cid) : 0;
|
const cid = wrapper.dataset.cid ? Number(wrapper.dataset.cid) : 0;
|
||||||
const code = (cid && _previewCodeMap.has(cid)) ? _previewCodeMap.get(cid) : wrapper.querySelector('code').textContent;
|
const code = (cid && _previewCodeMap.has(cid)) ? _previewCodeMap.get(cid) : wrapper.querySelector('code')?.textContent;
|
||||||
navigator.clipboard.writeText(code).then(() => {
|
const defaultLabel = btn.dataset.defaultLabel || btn.textContent || 'Copy';
|
||||||
|
btn.dataset.defaultLabel = defaultLabel;
|
||||||
|
btn.disabled = true;
|
||||||
|
const copied = await copyTextToClipboard(code, '代码已复制');
|
||||||
|
btn.disabled = false;
|
||||||
|
if (!copied) return;
|
||||||
|
if (btn._copyResetTimer) clearTimeout(btn._copyResetTimer);
|
||||||
btn.textContent = 'Copied!';
|
btn.textContent = 'Copied!';
|
||||||
setTimeout(() => btn.textContent = 'Copy', 1500);
|
btn._copyResetTimer = setTimeout(() => {
|
||||||
});
|
btn.textContent = btn.dataset.defaultLabel || 'Copy';
|
||||||
|
btn._copyResetTimer = null;
|
||||||
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.ccTogglePreview = function (btn) {
|
window.ccTogglePreview = function (btn, event) {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
const wrapper = btn.closest('.code-block-wrapper');
|
const wrapper = btn.closest('.code-block-wrapper');
|
||||||
const inPreview = wrapper.classList.contains('preview-mode');
|
const inPreview = wrapper.classList.contains('preview-mode');
|
||||||
if (inPreview) {
|
if (inPreview) {
|
||||||
@@ -3617,6 +3919,7 @@
|
|||||||
toolsDiv.className = 'msg-tools';
|
toolsDiv.className = 'msg-tools';
|
||||||
bubble.appendChild(textDiv);
|
bubble.appendChild(textDiv);
|
||||||
bubble.appendChild(toolsDiv);
|
bubble.appendChild(toolsDiv);
|
||||||
|
syncAssistantLastSectionButton(msgEl);
|
||||||
messagesDiv.appendChild(msgEl);
|
messagesDiv.appendChild(msgEl);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
return true;
|
return true;
|
||||||
@@ -3666,6 +3969,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
streamEl.removeAttribute('id');
|
streamEl.removeAttribute('id');
|
||||||
|
syncAssistantLastSectionButton(streamEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
@@ -3702,6 +4006,7 @@
|
|||||||
if (!textDiv) { textDiv = bubble; }
|
if (!textDiv) { textDiv = bubble; }
|
||||||
textDiv.innerHTML = renderMarkdown(pendingText);
|
textDiv.innerHTML = renderMarkdown(pendingText);
|
||||||
}
|
}
|
||||||
|
syncAssistantLastSectionButton(streamEl);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3877,6 +4182,7 @@
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
const isCrossConversation = !!meta.crossConversation;
|
const isCrossConversation = !!meta.crossConversation;
|
||||||
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
|
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
|
||||||
|
const canCollapseCrossConversationReply = role === 'assistant' && isCrossConversationReply;
|
||||||
const resolvedMessageId = meta?.messageId || meta?.id || createLocalId('user');
|
const resolvedMessageId = meta?.messageId || meta?.id || createLocalId('user');
|
||||||
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}`;
|
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}`;
|
||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
@@ -3932,6 +4238,28 @@
|
|||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = 'msg-bubble';
|
bubble.className = 'msg-bubble';
|
||||||
|
let crossConversationReplyCollapseKey = '';
|
||||||
|
let crossConversationReplyToggle = null;
|
||||||
|
let crossConversationReplyBody = null;
|
||||||
|
|
||||||
|
const applyCrossConversationReplyCollapseState = (collapsed) => {
|
||||||
|
if (!crossConversationReplyToggle || !crossConversationReplyBody) return;
|
||||||
|
div.classList.toggle('cross-conversation-collapsed', collapsed);
|
||||||
|
bubble.dataset.collapsed = collapsed ? 'true' : 'false';
|
||||||
|
crossConversationReplyBody.hidden = collapsed;
|
||||||
|
crossConversationReplyToggle.textContent = collapsed ? '展开' : '收起';
|
||||||
|
crossConversationReplyToggle.title = collapsed ? '展开返回消息' : '收起返回消息';
|
||||||
|
crossConversationReplyToggle.setAttribute('aria-label', collapsed ? '展开返回消息' : '收起返回消息');
|
||||||
|
crossConversationReplyToggle.setAttribute('aria-expanded', String(!collapsed));
|
||||||
|
updateScrollbar();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (canCollapseCrossConversationReply) {
|
||||||
|
crossConversationReplyCollapseKey = getCrossConversationReplyCollapseKey(meta);
|
||||||
|
if (crossConversationReplyCollapseKey) {
|
||||||
|
div.dataset.crossConversationReplyKey = crossConversationReplyCollapseKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isCrossConversation) {
|
if (isCrossConversation) {
|
||||||
const source = meta.crossConversation || {};
|
const source = meta.crossConversation || {};
|
||||||
@@ -3960,6 +4288,27 @@
|
|||||||
sourceMeta.appendChild(copyBtn);
|
sourceMeta.appendChild(copyBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canCollapseCrossConversationReply) {
|
||||||
|
const replyTimeText = formatCrossConversationReplyTime(meta.timestamp || source.processedAt || source.sentAt);
|
||||||
|
if (replyTimeText) {
|
||||||
|
const time = document.createElement('span');
|
||||||
|
time.className = 'cross-conversation-time';
|
||||||
|
time.textContent = replyTimeText;
|
||||||
|
sourceMeta.appendChild(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
crossConversationReplyToggle = document.createElement('button');
|
||||||
|
crossConversationReplyToggle.type = 'button';
|
||||||
|
crossConversationReplyToggle.className = 'cross-conversation-collapse-btn';
|
||||||
|
crossConversationReplyToggle.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const collapsed = !div.classList.contains('cross-conversation-collapsed');
|
||||||
|
setCrossConversationReplyCollapsed(crossConversationReplyCollapseKey, collapsed);
|
||||||
|
applyCrossConversationReplyCollapseState(collapsed);
|
||||||
|
});
|
||||||
|
sourceMeta.appendChild(crossConversationReplyToggle);
|
||||||
|
}
|
||||||
|
|
||||||
bubble.appendChild(sourceMeta);
|
bubble.appendChild(sourceMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3994,15 +4343,29 @@
|
|||||||
const mentionsStrip = renderComposerMentionsStrip(meta);
|
const mentionsStrip = renderComposerMentionsStrip(meta);
|
||||||
if (mentionsStrip) bubble.appendChild(mentionsStrip);
|
if (mentionsStrip) bubble.appendChild(mentionsStrip);
|
||||||
} else {
|
} else {
|
||||||
renderAssistantContent(bubble, content);
|
const assistantContentTarget = canCollapseCrossConversationReply ? document.createElement('div') : bubble;
|
||||||
|
if (canCollapseCrossConversationReply) {
|
||||||
|
assistantContentTarget.className = 'cross-conversation-reply-body';
|
||||||
|
crossConversationReplyBody = assistantContentTarget;
|
||||||
|
}
|
||||||
|
renderAssistantContent(assistantContentTarget, content);
|
||||||
if (attachments.length > 0) {
|
if (attachments.length > 0) {
|
||||||
bubble.insertAdjacentHTML('beforeend', renderAttachmentPreviews(attachments));
|
assistantContentTarget.insertAdjacentHTML('beforeend', renderAttachmentPreviews(attachments));
|
||||||
|
}
|
||||||
|
if (canCollapseCrossConversationReply) {
|
||||||
|
bubble.appendChild(assistantContentTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hydrateAttachmentPreviews(bubble, attachments);
|
hydrateAttachmentPreviews(bubble, attachments);
|
||||||
div.appendChild(avatar);
|
div.appendChild(avatar);
|
||||||
div.appendChild(bubble);
|
div.appendChild(bubble);
|
||||||
|
if (role === 'assistant') {
|
||||||
|
syncAssistantLastSectionButton(div);
|
||||||
|
}
|
||||||
|
if (canCollapseCrossConversationReply) {
|
||||||
|
applyCrossConversationReplyCollapseState(isCrossConversationReplyCollapsed(crossConversationReplyCollapseKey));
|
||||||
|
}
|
||||||
if (role === 'user' && meta.codexAppSteerStatus) {
|
if (role === 'user' && meta.codexAppSteerStatus) {
|
||||||
setCodexAppSteerStatusElement(div, meta.codexAppSteerStatus, meta.codexAppSteerMessage);
|
setCodexAppSteerStatusElement(div, meta.codexAppSteerStatus, meta.codexAppSteerMessage);
|
||||||
}
|
}
|
||||||
@@ -4699,6 +5062,7 @@
|
|||||||
const el = createMsgElement(m.role, m.content, m.attachments || [], m);
|
const el = createMsgElement(m.role, m.content, m.attachments || [], m);
|
||||||
if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
|
if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
|
||||||
const bubble = el.querySelector('.msg-bubble');
|
const bubble = el.querySelector('.msg-bubble');
|
||||||
|
const toolMount = bubble.querySelector(':scope > .cross-conversation-reply-body') || bubble;
|
||||||
const FOLD_AT = 3;
|
const FOLD_AT = 3;
|
||||||
let grouped = false;
|
let grouped = false;
|
||||||
const mergedCollabTool = mergeCollabAgentTools(m.toolCalls);
|
const mergedCollabTool = mergeCollabAgentTools(m.toolCalls);
|
||||||
@@ -4711,9 +5075,9 @@
|
|||||||
const details = createToolCallElement(tc.id || `saved-${Math.random().toString(36).slice(2)}`, tc, true);
|
const details = createToolCallElement(tc.id || `saved-${Math.random().toString(36).slice(2)}`, tc, true);
|
||||||
|
|
||||||
// 散落的 .tool-call 达到 FOLD_AT 个时,移入唯一 .tool-group
|
// 散落的 .tool-call 达到 FOLD_AT 个时,移入唯一 .tool-group
|
||||||
const loose = Array.from(bubble.children).filter(isGroupableToolCall);
|
const loose = Array.from(toolMount.children).filter(isGroupableToolCall);
|
||||||
if (loose.length >= FOLD_AT) {
|
if (loose.length >= FOLD_AT) {
|
||||||
let group = bubble.querySelector(':scope > .tool-group');
|
let group = toolMount.querySelector(':scope > .tool-group');
|
||||||
if (!group) {
|
if (!group) {
|
||||||
group = document.createElement('details');
|
group = document.createElement('details');
|
||||||
group.className = 'tool-group';
|
group.className = 'tool-group';
|
||||||
@@ -4723,20 +5087,20 @@
|
|||||||
const inner = document.createElement('div');
|
const inner = document.createElement('div');
|
||||||
inner.className = 'tool-group-inner';
|
inner.className = 'tool-group-inner';
|
||||||
group.appendChild(inner);
|
group.appendChild(inner);
|
||||||
bubble.insertBefore(group, bubble.firstChild);
|
toolMount.insertBefore(group, toolMount.firstChild);
|
||||||
grouped = true;
|
grouped = true;
|
||||||
}
|
}
|
||||||
const inner = group.querySelector('.tool-group-inner');
|
const inner = group.querySelector('.tool-group-inner');
|
||||||
loose.forEach(c => inner.appendChild(c));
|
loose.forEach(c => inner.appendChild(c));
|
||||||
_refreshGroupSummary(group);
|
_refreshGroupSummary(group);
|
||||||
}
|
}
|
||||||
bubble.appendChild(details);
|
toolMount.appendChild(details);
|
||||||
}
|
}
|
||||||
// 结束时若出现过父目录,收尾散落项
|
// 结束时若出现过父目录,收尾散落项
|
||||||
if (grouped) {
|
if (grouped) {
|
||||||
const loose = Array.from(bubble.children).filter(isGroupableToolCall);
|
const loose = Array.from(toolMount.children).filter(isGroupableToolCall);
|
||||||
if (loose.length > 0) {
|
if (loose.length > 0) {
|
||||||
const group = bubble.querySelector(':scope > .tool-group');
|
const group = toolMount.querySelector(':scope > .tool-group');
|
||||||
if (group) {
|
if (group) {
|
||||||
const inner = group.querySelector('.tool-group-inner');
|
const inner = group.querySelector('.tool-group-inner');
|
||||||
loose.forEach(c => inner.appendChild(c));
|
loose.forEach(c => inner.appendChild(c));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
document.documentElement.dataset.dividerTime = dividerTime;
|
document.documentElement.dataset.dividerTime = dividerTime;
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="style.css?v=20260617-skill-openai-yaml">
|
<link rel="stylesheet" href="style.css?v=20260621-cross-reply-collapse-last-section-offset">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -154,6 +154,6 @@
|
|||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
<script src="app.js?v=20260618-mobile-session-switch"></script>
|
<script src="app.js?v=20260621-cross-reply-collapse-last-section-offset"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
124
public/style.css
124
public/style.css
@@ -2279,19 +2279,24 @@ body.session-loading-active {
|
|||||||
border-color: rgba(93, 138, 84, 0.28);
|
border-color: rgba(93, 138, 84, 0.28);
|
||||||
}
|
}
|
||||||
.msg.cross-conversation-reply .cross-conversation-meta,
|
.msg.cross-conversation-reply .cross-conversation-meta,
|
||||||
.msg.cross-conversation-reply .cross-conversation-id-btn {
|
.msg.cross-conversation-reply .cross-conversation-id-btn,
|
||||||
|
.msg.cross-conversation-reply .cross-conversation-collapse-btn,
|
||||||
|
.msg.cross-conversation-reply .cross-conversation-time {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
.msg.cross-conversation-reply .cross-conversation-id-btn {
|
.msg.cross-conversation-reply .cross-conversation-id-btn,
|
||||||
|
.msg.cross-conversation-reply .cross-conversation-collapse-btn {
|
||||||
border-color: rgba(93, 138, 84, 0.28);
|
border-color: rgba(93, 138, 84, 0.28);
|
||||||
}
|
}
|
||||||
.msg.cross-conversation-reply .cross-conversation-id-btn:hover {
|
.msg.cross-conversation-reply .cross-conversation-id-btn:hover,
|
||||||
|
.msg.cross-conversation-reply .cross-conversation-collapse-btn:hover {
|
||||||
background: rgba(93, 138, 84, 0.14);
|
background: rgba(93, 138, 84, 0.14);
|
||||||
}
|
}
|
||||||
.cross-conversation-meta {
|
.cross-conversation-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
row-gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
color: var(--info);
|
color: var(--info);
|
||||||
@@ -2299,10 +2304,19 @@ body.session-loading-active {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
.cross-conversation-label {
|
.cross-conversation-label {
|
||||||
|
flex: 1 1 180px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.cross-conversation-id-btn {
|
.cross-conversation-time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.cross-conversation-id-btn,
|
||||||
|
.cross-conversation-collapse-btn {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 1px solid rgba(91, 126, 161, 0.24);
|
border: 1px solid rgba(91, 126, 161, 0.24);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -2313,15 +2327,72 @@ body.session-loading-active {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.cross-conversation-id-btn:hover {
|
.cross-conversation-collapse-btn {
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
.cross-conversation-id-btn:hover,
|
||||||
|
.cross-conversation-collapse-btn:hover {
|
||||||
background: rgba(91, 126, 161, 0.14);
|
background: rgba(91, 126, 161, 0.14);
|
||||||
}
|
}
|
||||||
|
.cross-conversation-collapse-btn:focus-visible {
|
||||||
|
outline: 2px solid rgba(93, 138, 84, 0.28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.msg.cross-conversation-collapsed .cross-conversation-meta {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.msg.cross-conversation-collapsed .msg-last-section-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.msg.assistant .msg-bubble {
|
.msg.assistant .msg-bubble {
|
||||||
background: var(--bg-bubble-assistant);
|
background: var(--bg-bubble-assistant);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
.msg-last-section-btn {
|
||||||
|
appearance: none;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin: 8px 0 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.72;
|
||||||
|
transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.msg-last-section-btn[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.msg-last-section-btn svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.msg-last-section-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.msg-last-section-btn:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
outline: 2px solid rgba(91, 126, 161, 0.28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.msg-last-section-focus {
|
||||||
|
animation: lastSectionFocus 1.1s ease;
|
||||||
|
}
|
||||||
|
@keyframes lastSectionFocus {
|
||||||
|
0% { background: rgba(91, 126, 161, 0.18); }
|
||||||
|
100% { background: transparent; }
|
||||||
|
}
|
||||||
.msg.assistant .msg-mention-chip {
|
.msg.assistant .msg-mention-chip {
|
||||||
border-color: rgba(48, 62, 82, 0.14);
|
border-color: rgba(48, 62, 82, 0.14);
|
||||||
background: rgba(91, 126, 161, 0.07);
|
background: rgba(91, 126, 161, 0.07);
|
||||||
@@ -2577,26 +2648,50 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: #2b2b2b;
|
background: #2b2b2b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
.code-block-header > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.code-block-actions {
|
.code-block-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.code-copy-btn, .code-preview-btn {
|
.code-copy-btn, .code-preview-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 28px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #999;
|
color: #999;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 2px 8px;
|
line-height: 1;
|
||||||
|
padding: 4px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.code-copy-btn:hover, .code-preview-btn:hover { color: #fff; background: #444; }
|
.code-copy-btn:hover, .code-preview-btn:hover { color: #fff; background: #444; }
|
||||||
|
.code-copy-btn:focus-visible, .code-preview-btn:focus-visible {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.45);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.code-copy-btn:disabled, .code-preview-btn:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
.code-preview-btn { border: 1px solid #555; }
|
.code-preview-btn { border: 1px solid #555; }
|
||||||
.code-block-wrapper pre {
|
.code-block-wrapper pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -2628,6 +2723,20 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
|||||||
.code-block-wrapper.preview-mode .code-preview-pane { display: block; }
|
.code-block-wrapper.preview-mode .code-preview-pane { display: block; }
|
||||||
.code-block-wrapper.preview-mode pre { display: none; }
|
.code-block-wrapper.preview-mode pre { display: none; }
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.code-block-header {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
.code-block-actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.code-copy-btn, .code-preview-btn {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Tool calls */
|
/* Tool calls */
|
||||||
.msg-tools {
|
.msg-tools {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -5317,7 +5426,8 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
|||||||
box-shadow: var(--dark-shadow-soft);
|
box-shadow: var(--dark-shadow-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .cross-conversation-id-btn {
|
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .cross-conversation-id-btn,
|
||||||
|
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .cross-conversation-collapse-btn {
|
||||||
background: var(--dark-panel-soft);
|
background: var(--dark-panel-soft);
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user