v0.2.0
This commit is contained in:
1 parent
731768deb9
commit
863abe7c76
18 files changed
+2740
-114
No files matched your search
@@ -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 @@
|
||||
{"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}}}}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,防止重复注入
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user