This commit is contained in:
minorQAQ committed 2026-08-14 15:35:05 +08:00
1 parent 731768deb9
commit 863abe7c76
18 files changed
+2740 -114

No files matched your search

+3 -1
View File
@@ -1,6 +1,8 @@
{
"current": "",
"current": "C:\\Users\\86166\\Desktop\\新建文件夹",
"list": [
"C:\\Users\\86166\\Desktop\\新建文件夹",
"C:\\Users\\86166\\Desktop",
"C:\\Users\\86166\\Desktop\\Agent_Learning_minor\\Agent\\src\\workspace"
],
"access_mode": "full"
+1 -1
View File
@@ -1 +1 @@
{"hours": {"2026-08-12T22": {"total": 41613, "by_model": {"Qwen3.5-122B-A10B": 41613}}}}
{"hours": {"2026-08-12T22": {"total": 41613, "by_model": {"Qwen3.5-122B-A10B": 41613}}, "2026-08-14T09": {"total": 535476, "by_model": {"Qwen3.5-122B-A10B": 535476}}, "2026-08-14T10": {"total": 91339, "by_model": {"Qwen3.5-122B-A10B": 91339}}, "2026-08-14T11": {"total": 67389, "by_model": {"Qwen3.5-122B-A10B": 67389}}}}
+17
View File
@@ -1018,6 +1018,18 @@ if (window.electronAPI?.minimizeWindow) {
initTheme();
initI18n();
// ===== 启动玻璃层过渡(毛玻璃 + 中央图标) =====
// 加载完成后:图标向下滑出、毛玻璃渐变消失,露出完整应用界面
function hideAppSplash() {
const splash = document.getElementById("appSplash");
if (!splash || splash.classList.contains("app-splash--out")) return;
splash.classList.add("app-splash--out");
// 动画结束后移除覆盖层(放行对底层界面的交互)
setTimeout(() => {
if (splash.parentNode) splash.parentNode.removeChild(splash);
}, 800);
}
// ===== 启动 =====
bootstrap().then(() => {
initTokenRing();
@@ -1028,10 +1040,15 @@ bootstrap().then(() => {
initThinkLevel();
loadNickname();
initServicesStatus();
hideAppSplash();
}).catch((e) => {
const chatPlaceholder = $("chatPlaceholder");
if (chatPlaceholder) {
chatPlaceholder.hidden = false;
chatPlaceholder.innerHTML = `<h3>无法连接服务</h3><p>${escapeHtml(e.message)}</p><p class="muted">请从 Agent 目录执行:<code>uvicorn web.server:app --reload --host 127.0.0.1 --port 8765</code>,并保证 PYTHONPATH 包含 <code>src</code>。</p>`;
}
hideAppSplash();
});
// 兜底:若前端初始化异常导致 bootstrap 永不完成,超时后强制淡出玻璃层
setTimeout(hideAppSplash, 12000);
+26 -8
View File
@@ -14,6 +14,7 @@
border-radius: var(--radius, 12px);
box-shadow: 0 8px 32px var(--overlay-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--accent);
overflow: hidden;
transition: all 0.3s ease;
color: var(--text);
@@ -88,6 +89,7 @@
border-radius: 10px;
overflow: hidden;
background: var(--surface-hover);
box-shadow: 0 1px 4px var(--shadow);
transition: opacity 0.3s;
}
@@ -96,6 +98,11 @@
pointer-events: none;
}
.pending-overlay-card--resolved .pending-overlay-btn,
.pending-overlay-card--resolved .pending-overlay-send-btn {
pointer-events: none;
}
.pending-overlay-card-header {
display: flex;
align-items: center;
@@ -149,9 +156,9 @@
/* 工作空间越界审批提示(访问策略 policy_note) */
.pending-overlay-note {
font-size: 0.78rem;
color: #b45309;
background: color-mix(in srgb, #f59e0b 12%, transparent);
border: 1px solid color-mix(in srgb, #f59e0b 45%, transparent);
color: var(--warning, #b45309);
background: var(--warning-bg, color-mix(in srgb, #f59e0b 12%, transparent));
border: 1px solid color-mix(in srgb, var(--warning, #f59e0b) 45%, transparent);
border-radius: 8px;
padding: 0.4rem 0.55rem;
margin-bottom: 0.5rem;
@@ -160,7 +167,7 @@
word-break: break-all;
}
/* ===== tool_call 卡片:参数只读展示 ===== */
/* ===== tool_call 卡片:参数摘要只读展示 ===== */
.pending-overlay-tool-name {
font-size: 0.8rem;
font-weight: 600;
@@ -169,18 +176,19 @@
}
.pending-overlay-tool-args {
font-size: 0.76rem;
font-size: 0.78rem;
color: var(--text);
background: color-mix(in srgb, var(--text) 6%, transparent);
border: 1px solid var(--border);
background: color-mix(in srgb, var(--accent) 5%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
border-radius: 8px;
padding: 0.4rem 0.55rem;
margin-bottom: 0.5rem;
white-space: pre-wrap;
word-break: break-all;
max-height: 160px;
max-height: 120px;
overflow-y: auto;
font-family: var(--font-family, inherit);
line-height: 1.55;
}
/* ===== 按钮行(同意 / 拒绝 / 跳过)===== */
@@ -206,6 +214,16 @@
.pending-overlay-btn:active {
transform: translateY(1px);
}
.pending-overlay-btn:focus-visible,
.pending-overlay-send-btn:focus-visible,
.pending-overlay-submit-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.pending-overlay-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pending-overlay-btn--approve {
background: var(--accent);
+130
View File
@@ -0,0 +1,130 @@
/* splash.css — 启动玻璃层(毛玻璃 + 中央图标)
*
* 被两个页面共用:
* - electron/splash.html(本地过渡页,FastAPI 就绪前显示,file:// 加载)
* - index.html 的 #appSplash 覆盖层(应用页加载完成后淡出,露出完整界面)
*
* 主题随动:
* - splash.html 由 main.js 读取 theme_config.json 后,通过 query 注入
* theme=dark/light,页面在 <html> 上挂 theme-dark / theme-light 类
* - index.html 由 themes.js 在 <body> 上挂 theme-dark / theme-light 类
* 两类名都通过 --splash-* 自定义属性切换毛玻璃底色与图标颜色。
*
* 退出动画(通过 .app-splash--out 触发):
* - 图标向下滑出消失
* - 毛玻璃层透明度、模糊度渐变消失,底层内容由模糊变清晰
*/
/* 默认按深色主题(与应用默认 vscode-dark、窗口默认底色一致) */
:root {
--splash-tint: rgba(8, 10, 20, 0.62);
--splash-icon-filter: invert(1) drop-shadow(0 6px 16px rgba(0, 0, 0, 0.5));
--splash-page-bg: radial-gradient(1200px 800px at 30% 20%, #2b2f57 0%, #1a1a2e 45%, #10101f 100%);
}
.theme-light {
--splash-tint: rgba(236, 240, 248, 0.55);
--splash-icon-filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.25));
--splash-page-bg: radial-gradient(1200px 800px at 30% 20%, #dfe5f2 0%, #eef1f7 45%, #ffffff 100%);
}
/* 本地过渡页(electron/splash.html)的底色,供玻璃层模糊 */
.splash-page {
margin: 0;
min-height: 100%;
min-height: 100dvh;
background: var(--splash-page-bg);
}
/* 全屏毛玻璃层 */
.app-splash {
position: fixed;
inset: 0;
z-index: 100000; /* 盖住自绘标题栏(z-index: 9999)与全部界面,加载期间无任何按钮 */
display: flex;
align-items: center;
justify-content: center;
background: var(--splash-tint);
-webkit-backdrop-filter: blur(24px) saturate(130%);
backdrop-filter: blur(24px) saturate(130%);
opacity: 1;
pointer-events: auto;
overflow: hidden;
-webkit-app-region: drag; /* 无边框窗口:玻璃层可拖动 */
transition:
opacity 0.65s ease,
-webkit-backdrop-filter 0.65s ease,
backdrop-filter 0.65s ease;
will-change: opacity, backdrop-filter;
}
/* 斜向光带扫过(“刷新”质感) */
.app-splash::before {
content: "";
position: absolute;
top: -60%;
left: -30%;
width: 60%;
height: 220%;
background: linear-gradient(
105deg,
transparent 42%,
rgba(255, 255, 255, 0.1) 50%,
transparent 58%
);
transform: rotate(12deg) translateX(-160%);
animation: splash-sweep 2.4s ease-in-out infinite;
pointer-events: none;
}
@keyframes splash-sweep {
0% {
transform: rotate(12deg) translateX(-160%);
}
55%,
100% {
transform: rotate(12deg) translateX(320%);
}
}
/* 中央图标(无边框/无卡片,直接浮在毛玻璃上;颜色随主题反色) */
.app-splash-icon {
width: 120px;
height: 120px;
filter: var(--splash-icon-filter);
user-select: none;
animation: splash-breathe 2.2s ease-in-out infinite;
}
@keyframes splash-breathe {
0%,
100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-6px) scale(1.02);
}
}
/* 退出动画:玻璃层淡出 + 图标向下滑出 */
.app-splash--out {
opacity: 0;
-webkit-backdrop-filter: blur(0px);
backdrop-filter: blur(0px);
pointer-events: none;
}
.app-splash--out .app-splash-icon {
animation: splash-icon-out 0.7s cubic-bezier(0.55, 0, 0.85, 0.36) forwards;
}
@keyframes splash-icon-out {
to {
transform: translateY(170px) scale(0.96);
opacity: 0;
}
}
/* 跟随系统“减少动态效果”偏好 */
@media (prefers-reduced-motion: reduce) {
.app-splash,
.app-splash::before,
.app-splash-icon {
animation: none;
}
}
+17
View File
@@ -3,12 +3,29 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<script>
// 首帧主题同步:main.js 启动时通过 ?theme=dark|light 传入当前主题,
// 这里在页面渲染前挂好类与底色变量,避免从本地过渡页切到应用页时出现深色闪屏
(function () {
var m = new URLSearchParams(location.search).get("theme");
if (m === "light" || m === "dark") {
var el = document.documentElement;
el.classList.add(m === "light" ? "theme-light" : "theme-dark");
el.style.setProperty("--edit-bg", m === "light" ? "#ffffff" : "#1a1a2e");
}
})();
</script>
<!-- 本地字体(离线可用,不再依赖 Google Fonts CDN) -->
<link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="icon" href="/image/favicon.svg" sizes="any" />
<link rel="stylesheet" href="/app.css?v=6" />
<link rel="stylesheet" href="/css/splash.css?v=1" />
</head>
<body>
<!-- 启动玻璃层:毛玻璃 + 中央图标(无边框、无任何按钮),加载完成后由 app.js 播放淡出动画 -->
<div class="app-splash" id="appSplash" aria-hidden="true">
<img class="app-splash-icon" src="/image/icon.svg" alt="" draggable="false" />
</div>
<div class="bg-scene" id="bgScene" aria-hidden="true">
<div class="bg-image" id="bgImageLayer"></div>
<div class="bg-vignette" aria-hidden="true"></div>
+70
View File
@@ -710,6 +710,8 @@ function renderUserFolderRefCard(folderPath) {
function renderUserFileRefCard(filePath) {
const card = document.createElement("div");
card.className = "user-file-card";
card.draggable = true;
card.title = filePath;
const icon = document.createElement("img");
icon.className = "user-file-card-icon";
@@ -734,9 +736,77 @@ function renderUserFileRefCard(filePath) {
info.appendChild(extEl);
card.appendChild(icon);
card.appendChild(info);
// 点击卡片 → 浮窗预览内容
card.addEventListener("click", (e) => {
e.stopPropagation();
previewRefFile(filePath);
});
// 拖拽:支持从聊天区拖拽整张文件引用卡片到输入区(与文件夹引用一致)
card.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("application/doc-path", filePath);
e.dataTransfer.setData("text/plain", safeName);
e.dataTransfer.effectAllowed = "copy";
});
return card;
}
/* ===== 浮窗预览本地路径文件(@file: 引用卡片点击) ===== */
async function previewRefFile(filePath) {
const fileName = filePath.split(/[/\\]/).pop() || filePath;
// 有工作区时复用 edit 模式的文档预览(图片/HTML/表格/二进制/文本统一处理)
if (state._docRootPath) {
try {
const { loadDocContent } = await import('./edit-mode.js');
await loadDocContent(filePath);
return;
} catch { /* 回退到直接读取 */ }
}
const { readTextFile, readBinaryFile } = await import('./electron-api.js');
const { openFilePreview } = await import('./file-preview.js');
// 图片
if (/\.(png|jpe?g|gif|webp|bmp|ico)$/i.test(fileName)) {
try {
const res = await readBinaryFile(filePath, state._docRootPath);
if (res && res.status === 'ok' && res.data) {
showImagePreview(`data:${res.mime || 'image/png'};base64,${res.data}`);
return;
}
} catch { /* 继续 */ }
openFilePreview(filePath, fileName, '(无法读取文件)');
return;
}
// 表格(xlsx/xls):后端提取为 TSV 后按列着色预览
if (/\.(xlsx|xls)$/i.test(fileName)) {
try {
const { api } = await import('./api.js');
const tsv = await api('/api/table/text?path=' + encodeURIComponent(filePath));
openFilePreview(filePath, fileName, (tsv && tsv.content) || '');
} catch (err) {
openFilePreview(filePath, fileName, '(加载失败: ' + (err.message || err) + ')');
}
return;
}
// 文本/代码
try {
const res = await readTextFile(filePath, state._docRootPath);
if (res && res.status === 'ok') {
openFilePreview(filePath, fileName, res.content || '');
} else {
openFilePreview(filePath, fileName, '(无法读取文件)');
}
} catch (err) {
openFilePreview(filePath, fileName, '(加载失败: ' + (err.message || err) + ')');
}
}
/* ===== 从工具文件 URL 中提取文件名 ===== */
function extractToolFileName(url) {
if (!url) return "文件";
+4 -31
View File
@@ -92,11 +92,8 @@ export function switchToMode(mode) {
_modeSwitchCooldown = true;
setTimeout(() => { _modeSwitchCooldown = false; }, 300);
// Agent 运行中禁止切换到 cron/edit 模式,避免 chat 区域被替换导致工具调用和思考历史消失
if (state.sending && mode !== "chat") {
showToast("Agent 正在运行中,请等待完成后再切换模式");
return;
}
// 注:edit/chat/cron 模式均只影响左侧栏(文件树/会话列表/定时任务),
// 右侧聊天区保持不变,因此 Agent 运行中也可以自由切换,无需等待
state.mode = mode;
if (mode === "edit") {
state.editMode = true;
@@ -1099,21 +1096,6 @@ export function clearRefChips() {
import('./chat-render.js').then(m => { if (m.renderAttachmentChips) m.renderAttachmentChips(); });
}
/** 将 pendingRefs 转为发送给 AI 的文本引用 */
function _refsToText() {
if (!state.pendingRefs.length) return '';
const root = state._docRootPath ? state._docRootPath.replace(/\\/g, '/').replace(/\/$/, '') : '';
return state.pendingRefs.map((ref) => {
// 外部引用路径已是绝对路径(统一为正斜杠),工作区引用需拼接 root
const absPath = ref.isExternal
? String(ref.path || '').replace(/\\/g, '/')
: (root + '/' + ref.path);
const prefix = ref.isFolder ? '@folder:' : '@file:';
const linePart = ref.startLine ? ` L${ref.startLine}-L${ref.endLine}` : '';
return `${prefix}${absPath}${linePart}`;
}).join('\n');
}
// ===== 清除状态 =====
export function clearEditState() {
// 清理该工作空间对应的快照子目录
@@ -1142,24 +1124,15 @@ export async function sendEditChat() {
if (!state.sessionId || state.sending) return;
const textInput = $('textInput');
const trimmed = textInput ? textInput.value.trim() : "";
const refsText = _refsToText();
const hasInput = trimmed || state.pendingFiles.length > 0 || state.pendingRefs.length > 0;
if (!hasInput) return;
// 保存原始文本,以便失败时恢复
const originalVal = textInput ? textInput.value : '';
// 将文档引用注入 textInput
if (refsText) {
textInput.value = originalVal ? originalVal + '\n\n' + refsText : refsText;
}
// 文档/文件夹引用(@file:/@folder:)由 sendChat 统一拼接为文本发送,
// 此处无需再注入 textInput,避免 chat/edit 双份引用
try {
const { sendChat } = await import('./send.js');
await sendChat();
} catch (err) {
// 发送失败:恢复原始文本,避免下次发送时重复注入 refs
if (textInput) textInput.value = originalVal;
throw err;
} finally {
// 无论成功失败都清除引用 chip,防止重复注入
+60 -13
View File
@@ -95,6 +95,53 @@ function buildPendingOverlay(pendingActions) {
return overlay;
}
/**
* 将 tool_call 的 args 规整为紧凑参数摘要。
* 后端包装为 {name, args:{...}},解包后:
* - 权限审查(oneLine=true)时只展示一条关键参数(如 file_path / command);
* - 普通确认时逐行展示 key: value(长值截断),避免整段 JSON 重复显示。
* @param {*} args - pending.args
* @param {boolean} oneLine - 是否只取关键参数一条(权限审查)
* @returns {string}
*/
function buildArgsSummary(args, oneLine) {
let obj = args;
if (typeof obj === "string") {
try { obj = JSON.parse(obj); } catch { /* 非 JSON 原样返回 */ }
}
if (obj === null || obj === undefined) return "";
if (typeof obj !== "object") return String(obj);
// 解包 {name, args} 包装(tool_call_utils 构造)
let inner = obj;
if (typeof obj.name === "string" && obj.args && typeof obj.args === "object" && !Array.isArray(obj.args)) {
inner = obj.args;
}
const fmt = (v) => {
if (v === null || v === undefined) return "";
const s = typeof v === "string" ? v : JSON.stringify(v);
return s.length > 160 ? s.slice(0, 157) + "..." : s;
};
const entries = Object.entries(inner || {}).filter(([, v]) => v !== null && v !== undefined && v !== "");
if (entries.length === 0) return "";
// 关键参数优先(权限审查时只显示这一条)
const PRIORITY = ["file_path", "path", "command", "cmd", "url", "dir", "folder", "query", "name", "action"];
const keyOf = (k) => {
const i = PRIORITY.indexOf(k);
return i === -1 ? 99 : i;
};
entries.sort((a, b) => keyOf(a[0]) - keyOf(b[0]));
if (oneLine) {
const [k, v] = entries[0];
return `${k}: ${fmt(v)}`;
}
return entries.map(([k, v]) => `${k}: ${fmt(v)}`).join("\n");
}
/**
* 创建决策按钮
* @param {string} label
@@ -143,7 +190,7 @@ function buildPendingCard(pending, isSubAgent) {
cardBody.appendChild(promptEl);
}
// ===== tool_call 类型:展示参数(只读)+ 决策按钮 =====
// ===== tool_call 类型:展示参数摘要(只读)+ 决策按钮 =====
if (pendingType === "tool_call") {
// 工作空间越界审批提示(访问策略注入的 policy_note)
if (pending.policy_note) {
@@ -152,24 +199,19 @@ function buildPendingCard(pending, isSubAgent) {
noteEl.textContent = "⚠️ 越界操作待审批:" + unescapeNewlines(pending.policy_note);
cardBody.appendChild(noteEl);
}
// 参数 JSON(只读)
if (pending.args !== undefined && pending.args !== null) {
let argsStr;
try {
argsStr = typeof pending.args === "string" ? pending.args : JSON.stringify(pending.args, null, 2);
} catch (_) {
argsStr = String(pending.args);
}
// 参数摘要:权限审查只展示一条关键参数;普通确认展示紧凑 key: value,
// 不再整段 JSON 重复(标题已含工具名)
const argsSummary = buildArgsSummary(pending.args, !!pending.policy_note);
if (argsSummary) {
const argsEl = document.createElement("div");
argsEl.className = "pending-overlay-tool-args";
argsEl.textContent = argsStr;
argsEl.textContent = argsSummary;
cardBody.appendChild(argsEl);
}
// 指令输入(可选,用于 approve 时附带说明)
// 指令输入(可选,用于 approve 时附带说明;不预填 JSON)
const textarea = document.createElement("textarea");
textarea.className = "pending-overlay-textarea";
textarea.placeholder = "可选:补充指令...";
textarea.value = pending.instruction || "";
textarea.rows = 2;
textarea.setAttribute("data-field", "instruction");
cardBody.appendChild(textarea);
@@ -212,7 +254,12 @@ function buildPendingCard(pending, isSubAgent) {
sendBtn.textContent = "确认";
sendBtn.addEventListener("click", () => {
const val = textarea.value.trim();
if (!val) return;
if (!val) {
// 空输入时给出明确反馈,避免误以为按钮无响应
showToast("请先输入内容再确认");
textarea.focus();
return;
}
submitSingle(pending.id, "approve", val, card);
});
inputRow.appendChild(sendBtn);
+19 -16
View File
@@ -1,8 +1,9 @@
// pending.js -- 人工请求决策提交(卡片渲染在 pending-overlay.js,数据源为 live snapshot)
import { $ } from './utils.js';
import { api } from './api.js';
import { state } from './state.js';
// 防重入:同一 approval_id 同时只允许一个提交请求(连点保护)
const _inFlight = new Set();
/**
* 提交一个等待中的人工请求的决策。
@@ -12,22 +13,24 @@ import { state } from './state.js';
* 浮窗由下一次 live snapshot 自动移除,最终结果由
* /api/chat/complete 或 /api/chat/stream 返回——无需重渲染消息、
* 无需重启 live 流、更不会清空已积累的工具调用列表。
*
* 注意:Agent 运行期间 state.sending 恒为 true(本轮仍在后台执行),
* 因此这里绝不能以 state.sending 作为拦截条件,否则浮窗按钮全部失效。
*/
async function handleHumanAction(requestId, decision, instruction, card) {
if (!requestId || state.sending) return;
state.sending = true; // 本轮仍在后台运行,保持发送按钮禁用
const sendBtn = $("sendBtn");
if (sendBtn) sendBtn.disabled = true;
if (card) card.classList.add("pending-action-card--resolved");
const fd = new FormData();
fd.append("session_id", state.sessionId);
fd.append("approval_id", requestId);
fd.append("decision", decision);
fd.append("instruction", instruction || "");
await api("/api/human-action", { method: "POST", body: fd });
// 注意:不恢复 state.sending——本轮任务尚未结束,按钮状态由 sendChat 完成时统一恢复
if (!requestId || _inFlight.has(requestId)) return;
_inFlight.add(requestId);
try {
const fd = new FormData();
fd.append("session_id", state.sessionId);
fd.append("approval_id", requestId);
fd.append("decision", decision);
fd.append("instruction", instruction || "");
await api("/api/human-action", { method: "POST", body: fd });
} finally {
_inFlight.delete(requestId);
}
// 注意:不修改 state.sending——本轮任务尚未结束,按钮状态由 sendChat 完成时统一恢复
}
export {
+39 -1
View File
@@ -151,20 +151,45 @@ function buildQuotesText() {
return quotes.map((q) => "【用户引用】\n" + q.text.trim()).join("\n\n");
}
/** 将待发送的文件/文件夹引用(拖拽产生的 @file:/@folder:)拼接为注入给 agent 的绝对路径文本 */
function buildRefsText() {
const refs = (state.pendingRefs || []).filter((r) => r && r.type !== "quote" && (r.path || r.isFolder));
if (refs.length === 0) return "";
const root = state._docRootPath ? state._docRootPath.replace(/\\/g, "/").replace(/\/$/, "") : "";
return refs.map((ref) => {
// 外部引用路径已是绝对路径(统一为正斜杠),工作区引用需拼接 root
const absPath = ref.isExternal
? String(ref.path || "").replace(/\\/g, "/")
: (root + "/" + ref.path);
const prefix = ref.isFolder ? "@folder:" : "@file:";
const linePart = ref.startLine ? ` L${ref.startLine}-L${ref.endLine}` : "";
return `${prefix}${absPath}${linePart}`;
}).join("\n");
}
/** 清除已发送的引用(pendingRefs 与 pendingFiles 中的 ref/quote 条目) */
function clearQuoteRefs() {
state.pendingRefs = (state.pendingRefs || []).filter((r) => r && r.type !== "quote");
state.pendingFiles = state.pendingFiles.filter((f) => !(f && f.__isRef && f.type === "ref/quote"));
}
/** 清除已发送的文件/文件夹引用(仅清理 @file:/@folder:,保留用户引用) */
function clearFileRefs() {
state.pendingRefs = (state.pendingRefs || []).filter((r) => r && r.type === "quote");
state.pendingFiles = state.pendingFiles.filter((f) => !(f && f.__isRef && f.type !== "ref/quote"));
}
async function sendChat() {
if (!state.sessionId || state.sending) return;
const myGen = ++_sendGeneration; // 标记本次 sendChat,防止旧实例的 finally/catch 污染状态
const trimmed = (textInput || {}).value ? textInput.value.trim() : "";
// 拖拽的文件/文件夹引用:以 @file:/@folder: 绝对路径形式并入发送文本(agent 收到路径本身)
const refsText = buildRefsText();
// 聊天区引用:以「用户引用」形式并入发送文本(agent 收到的内容为具体文字并注明来源)
const quotesText = buildQuotesText();
const effectiveText = quotesText ? (trimmed ? trimmed + "\n\n" + quotesText : quotesText) : trimmed;
const effectiveText = [trimmed, refsText, quotesText].filter(Boolean).join("\n\n");
const savedQuotes = (state.pendingRefs || []).filter((r) => r && r.type === "quote");
const savedFileRefs = (state.pendingRefs || []).filter((r) => r && r.type !== "quote");
if (!effectiveText && state.pendingFiles.length === 0) return;
// 首次发送时,输入框从居中移至底部
if (onSendStartFn) onSendStartFn();
@@ -238,6 +263,7 @@ async function sendChat() {
if (textInput) textInput.value = "";
state.pendingFiles.length = 0;
clearQuoteRefs();
clearFileRefs();
if (fileInput) fileInput.value = "";
if (renderAttachmentChipsFn) renderAttachmentChipsFn();
} else if (effectiveText) {
@@ -247,6 +273,7 @@ async function sendChat() {
}
if (textInput) textInput.value = "";
clearQuoteRefs();
clearFileRefs();
}
state.abortController = new AbortController();
@@ -269,6 +296,7 @@ async function sendChat() {
if (textInput) textInput.value = "";
state.pendingFiles.length = 0;
clearQuoteRefs();
clearFileRefs();
if (fileInput) fileInput.value = "";
if (renderAttachmentChipsFn) renderAttachmentChipsFn();
}
@@ -466,6 +494,16 @@ async function sendChat() {
state.pendingFiles.push(f);
}
});
}
if (savedFileRefs.length > 0) {
// 补回文件/文件夹引用(@file:/@folder:),避免失败后引用丢失
const haveFileRefs = new Set(state.pendingFiles.filter((f) => f && f.__isRef && f.type !== "ref/quote").map((f) => f.refPath));
savedFileRefs.forEach((f) => {
if (!haveFileRefs.has(f.refPath)) state.pendingFiles.push(f);
});
state.pendingRefs.push(...savedFileRefs);
}
if (savedQuotes.length > 0 || savedFileRefs.length > 0) {
if (renderAttachmentChipsFn) renderAttachmentChipsFn();
}
if (typingEl) _cleanupTypingEl(typingEl);
+5 -2
View File
@@ -723,10 +723,13 @@ export function applyTheme(themeId) {
document.dispatchEvent(new CustomEvent("theme-changed"));
}
/** 初始化:先用默认主题渲染,再从服务端 theme_config.json 恢复全部设置 */
/** 初始化:先用与首帧一致的主题渲染,再从服务端 theme_config.json 恢复全部设置 */
export function initTheme() {
_loadingTheme = true;
applyTheme(DEFAULT_THEME);
// 主进程启动时通过 ?theme= 指定了首帧主题(避免过渡闪屏),初始应用与之匹配的主题;
// 否则回退默认深色主题,随后 loadThemeConfig 从服务端恢复用户保存的主题
const firstPaintLight = document.documentElement.classList.contains("theme-light");
applyTheme(firstPaintLight ? "vscode-light" : DEFAULT_THEME);
applyBgImage("");
applyFontSetting("family", "");
applyFontSetting("size", "");
+11 -9
View File
@@ -381,10 +381,12 @@ def _inject_sub_tool_calls(
tc["sub_tool_calls"] = sub_tc
def _bootstrap() -> tuple[str, list[str], list[dict[str, Any]]]:
"""初始化默认会话:取最近会话或新建时间戳 ID 及其消息列表。
def _bootstrap(force_new: bool = False) -> tuple[str, list[str], list[dict[str, Any]]]:
"""初始化默认会话:默认新建空会话;force_new=False 时回退到最近会话。
输入: 无。
输入:
force_new: 为 True 时始终新建空会话(应用每次打开默认新建会话);
为 False 时取最近会话(删除会话后的回退逻辑)。
输出:
(session_id, sessions列表, messages列表)
@@ -396,13 +398,13 @@ def _bootstrap() -> tuple[str, list[str], list[dict[str, Any]]]:
可增加用户偏好默认会话逻辑。
"""
ids = ui.list_session_ids_ordered()
if ids:
sid = ids[0]
hist = ui.load_ui_messages_for_session(sid)
else:
if force_new or not ids:
sid = ui.new_timestamp_session_id()
hist = []
ids = [sid]
ids = [sid] + [x for x in ids if x != sid]
else:
sid = ids[0]
hist = ui.load_ui_messages_for_session(sid)
return sid, ids, hist
@@ -473,7 +475,7 @@ def api_bootstrap() -> dict[str, Any]:
可扩展性:
可返回用户配置、主题等元信息。
"""
sid, ids, hist = _bootstrap()
sid, ids, hist = _bootstrap(force_new=True)
from agent.history.tool_call_recorder import get_session_tokens
return {
"sessionId": sid,