13 Commits

Author SHA1 Message Date
251289a340 Merge branch 'feat/plugin-front-end' into merge/250105merge 2026-01-05 19:08:27 +08:00
cca82c7885 feat:将todo的需要改为勾选框
- 为后续的todo完成打勾做准备
2026-01-05 18:30:59 +08:00
3831de2849 fix: 修复 ICViewProvider 中的事件监听器内存泄漏问题
将 webview.onDidReceiveMessage 监听器添加到 context.subscriptions 中,
   确保在扩展停用时能够正确清理,避免潜在的内存泄漏。
2026-01-05 16:40:32 +08:00
0df529c4fd feat:实现思考的组件 2026-01-05 16:25:47 +08:00
5c53d7f0e9 feat:修改模式内容和增加icon 2026-01-05 16:22:52 +08:00
ef2a0dc16e feat:修改模型描述的展现形式和内容 2026-01-05 16:19:53 +08:00
5ce420295b feat:解决图片没有被打包进去的bug 2026-01-05 16:12:15 +08:00
1d7f3d7626 feat:添加上下文功能实现 2026-01-05 15:59:26 +08:00
9b0d2d5e01 feat:进度条收起的功能和发起对话才展示 2026-01-05 15:27:40 +08:00
27e3351b55 feat:输入框居中展示
- 点击历史记录和发起对话之后回到底部
2026-01-05 15:18:03 +08:00
de3e84aa4e feat:顶部添加进度条 2026-01-05 11:27:06 +08:00
8dc34ee435 feat:让用户看不懂的工具隐晦展示 2026-01-04 16:29:13 +08:00
d8cd86361e feat: 添加获取当前环境的功能以控制快速操作按钮的显示 2026-01-04 14:10:43 +08:00
15 changed files with 1770 additions and 162 deletions

View File

@ -101,7 +101,8 @@
"files": [ "files": [
"dist", "dist",
"media", "media",
"tools" "tools",
"src/assets"
], ],
"dependencies": { "dependencies": {
"@wavedrom/doppler": "^1.14.0", "@wavedrom/doppler": "^1.14.0",

View File

@ -82,6 +82,11 @@ export const agentIconSvg = `
*/ */
export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`; export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`;
/**
* Ask 模式图标 SVG
*/
export const askIconSvg = `<svg t="1767143500000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#8a8a8a"/><path d="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.5-111.6 40.7C369.2 344 352 380.7 352 420.4c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8 0-25.6 10.1-49.4 28.4-67.2 18.7-18.2 43.4-28.2 71.6-28.2s52.9 10 71.6 28.2c18.3 17.8 28.4 41.6 28.4 67.2 0 29.5-12.2 55.3-36.2 76.6-23.2 20.6-61.1 45.9-82.2 60.6-17.8 12.4-28.6 32.7-28.6 54.2V640c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-35.8c0-4.1 2.6-7.8 6.5-9.2 31.3-11.6 84.8-40.6 113.8-64.8 42.6-35.6 66.2-83.5 66.2-134.8 0-39.7-17.2-76.4-48.4-103.3z" fill="#8a8a8a"/><path d="M512 716m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#8a8a8a"/></svg>`;
/** /**
* 保存知识库图标 SVG * 保存知识库图标 SVG
*/ */

View File

@ -141,6 +141,9 @@ export async function showICHelperPanel(
// 切换到当前面板的任务上下文 // 切换到当前面板的任务上下文
historyManager.switchToPanelTask(panelId); historyManager.switchToPanelTask(panelId);
// 显示进度条
panel.webview.postMessage({ type: 'showProgress' });
handleUserMessage( handleUserMessage(
panel, panel,
message.text, message.text,
@ -278,6 +281,109 @@ export async function showICHelperPanel(
} }
} }
break; break;
// 添加文件上下文 - 显示工作区文件列表
case "addContextFile":
{
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showWarningMessage("请先打开一个工作区");
break;
}
// 获取工作区所有文件
const files = await vscode.workspace.findFiles(
"**/*",
"**/node_modules/**"
);
panel.webview.postMessage({
command: "showWorkspaceFileList",
files: files.map((uri) => ({
path: uri.fsPath,
relativePath: vscode.workspace.asRelativePath(uri),
})),
});
}
break;
// 添加文件夹上下文 - 显示工作区文件夹列表
case "addContextFolder":
{
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showWarningMessage("请先打开一个工作区");
break;
}
// 获取工作区所有文件夹
const fs = require("fs");
const path = require("path");
const folders: Array<{ path: string; relativePath: string }> = [];
function scanFolders(dir: string, baseDir: string) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
const fullPath = path.join(dir, item.name);
const relativePath = path.relative(baseDir, fullPath);
folders.push({ path: fullPath, relativePath });
scanFolders(fullPath, baseDir);
}
}
} catch (error) {
console.error("扫描文件夹失败:", error);
}
}
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
panel.webview.postMessage({
command: "showWorkspaceFolderList",
folders: folders,
});
}
break;
// 添加图片上下文
case "addContextImage":
{
const imageUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: "选择图片",
filters: {
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
},
});
if (imageUris && imageUris.length > 0) {
panel.webview.postMessage({
command: "contextImagesSelected",
images: imageUris.map((uri) => uri.fsPath),
});
}
}
break;
// 添加文档库上下文
case "addContextDocument":
{
const docUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: "选择文档",
filters: {
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
"所有文件": ["*"],
},
});
if (docUris && docUris.length > 0) {
panel.webview.postMessage({
command: "contextDocumentsSelected",
documents: docUris.map((uri) => uri.fsPath),
});
}
}
break;
// 新增:检查工作区状态 // 新增:检查工作区状态
case "checkWorkspace": case "checkWorkspace":
const hasWorkspace = !!( const hasWorkspace = !!(

View File

@ -160,13 +160,17 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
}); });
// 处理侧边栏的消息 // 处理侧边栏的消息
webviewView.webview.onDidReceiveMessage((message) => { webviewView.webview.onDidReceiveMessage(
if (message.command === "openChat") { (message) => {
vscode.commands.executeCommand("ic-coder.openChat"); if (message.command === "openChat") {
} else if (message.command === "login") { vscode.commands.executeCommand("ic-coder.openChat");
vscode.commands.executeCommand("ic-coder.login"); } else if (message.command === "login") {
} vscode.commands.executeCommand("ic-coder.login");
}); }
},
undefined,
this.context.subscriptions
);
} }
private getWebviewContent( private getWebviewContent(

View File

@ -96,6 +96,27 @@ export function getAgentCardStyles(): string {
padding: 8px; padding: 8px;
text-align: center; text-align: center;
} }
/* 低调显示的工具调用样式 */
.agent-step.low-profile {
opacity: 0.5;
font-size: 10px;
padding: 2px 6px;
background: transparent;
margin-bottom: 2px;
}
.agent-step.low-profile .step-icon {
opacity: 0.4;
font-size: 10px;
}
.agent-step.low-profile .step-name {
font-weight: 300;
color: var(--vscode-descriptionForeground);
opacity: 0.7;
}
.agent-step.low-profile .step-result {
opacity: 0.6;
font-size: 9px;
}
`; `;
} }
@ -119,8 +140,8 @@ export function getAgentCardScript(): string {
'queryKnowledgeSummary': '查询知识摘要', 'queryKnowledgeSummary': '查询知识摘要',
'queryRules': '查询规则', 'queryRules': '查询规则',
'setModule': '设置模块', 'setModule': '设置模块',
'addSignal': '添加信号', 'addSignal': '正在分析信号定义',
'addSignalExample': '添加信号示例', 'addSignalExample': '正在处理信号示例',
'validateKnowledgeGraph': '验证知识图谱', 'validateKnowledgeGraph': '验证知识图谱',
'querySignals': '查询信号', 'querySignals': '查询信号',
'addPlan': '添加计划', 'addPlan': '添加计划',
@ -151,7 +172,16 @@ export function getAgentCardScript(): string {
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄'; const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
const displayName = getAgentToolDisplayName(step.toolName); const displayName = getAgentToolDisplayName(step.toolName);
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : ''; const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
return \`<div class="agent-step"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`; // 为技术性工具调用添加低调样式(用户看不懂的)
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'spawnExplorer'
];
const stepClass = lowProfileTools.includes(step.toolName) ? 'agent-step low-profile' : 'agent-step';
return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
}).join(''); }).join('');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`

View File

@ -8,6 +8,12 @@
* - Agent: 智能体自主,自动执行大部分操作 * - Agent: 智能体自主,自动执行大部分操作
*/ */
import {
plannerIconSvg,
askIconSvg,
agentIconSvg,
} from "../constants/toolIcons";
/** /**
* 获取模式选择器的 HTML 内容 * 获取模式选择器的 HTML 内容
*/ */
@ -23,16 +29,25 @@ export function getModeSelectorContent(): string {
</div> </div>
<div class="mode-dropdown" id="modeDropdown"> <div class="mode-dropdown" id="modeDropdown">
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')"> <div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
<span class="mode-option-label">Plan</span> <div class="mode-option-header">
<span class="mode-option-desc">只读模式</span> <span class="mode-option-icon">${plannerIconSvg}</span>
<span class="mode-option-label">Plan</span>
</div>
<span class="mode-option-desc">仅根据需求生成设计文档,之后由用户决定下一步,可以提高工程质量</span>
</div> </div>
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')"> <div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
<span class="mode-option-label">Ask</span> <div class="mode-option-header">
<span class="mode-option-desc">逐个确认</span> <span class="mode-option-icon">${askIconSvg}</span>
<span class="mode-option-label">Ask</span>
</div>
<span class="mode-option-desc">仅给与智能体读权限,用于依据项目回答用户问题,或者与用户进行探讨</span>
</div> </div>
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')"> <div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
<span class="mode-option-label">Agent</span> <div class="mode-option-header">
<span class="mode-option-desc">智能体自主</span> <span class="mode-option-icon">${agentIconSvg}</span>
<span class="mode-option-label">Agent</span>
</div>
<span class="mode-option-desc">用于快速生成工程、调试修改现有代码</span>
</div> </div>
</div> </div>
</div> </div>
@ -83,7 +98,8 @@ export function getModeSelectorStyles(): string {
position: absolute; position: absolute;
bottom: calc(100% + 2px); bottom: calc(100% + 2px);
left: 0; left: 0;
min-width: 140px; min-width: 200px;
max-width: 300px;
background: var(--vscode-dropdown-background); background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border); border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px; border-radius: 4px;
@ -98,13 +114,12 @@ export function getModeSelectorStyles(): string {
/* 模式选择器的选项样式 */ /* 模式选择器的选项样式 */
.mode-option { .mode-option {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center; align-items: flex-start;
padding: 8px 12px; padding: 8px 12px;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; transition: background 0.2s ease;
white-space: nowrap;
} }
.mode-option:hover { .mode-option:hover {
background: rgba(128, 128, 128, 0.3); background: rgba(128, 128, 128, 0.3);
@ -112,13 +127,31 @@ export function getModeSelectorStyles(): string {
.mode-option.selected { .mode-option.selected {
background: rgba(64, 158, 255, 0.2); background: rgba(64, 158, 255, 0.2);
} }
.mode-option-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.mode-option-icon {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.mode-option-icon svg {
width: 16px;
height: 16px;
display: block;
}
.mode-option-label { .mode-option-label {
font-weight: 500; font-weight: 500;
} }
.mode-option-desc { .mode-option-desc {
font-size: 10px; font-size: 10px;
color: var(--vscode-descriptionForeground); color: var(--vscode-descriptionForeground);
margin-left: 12px; line-height: 1.4;
word-wrap: break-word;
white-space: normal;
} }
`; `;
} }
@ -157,9 +190,9 @@ export function getModeSelectorScript(): string {
// 更新 tooltip // 更新 tooltip
if (modeTooltip) { if (modeTooltip) {
const tooltipMap = { const tooltipMap = {
'plan': '只读模式 - 只能查询分析', 'plan': 'plan模式',
'ask': '逐个确认 - 每个写操作需确认', 'ask': 'ask模式',
'agent': '智能体自主模式',' 'agent': 'agent模式'
}; };
modeTooltip.textContent = tooltipMap[value] || '切换模式'; modeTooltip.textContent = tooltipMap[value] || '切换模式';
} }

View File

@ -7,14 +7,78 @@
*/ */
export function getContextButtonContent(): string { export function getContextButtonContent(): string {
return ` return `
<div class="tooltip"> <div class="context-selector-wrapper">
<button class="add-context-button" onclick="handleAddContext()"> <div class="tooltip">
<svg t="1766915545722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4994" width="200" height="200"> <button class="add-context-button" onclick="toggleContextMenu()">
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path> <svg t="1766915545722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4994" width="200" height="200">
</svg> <path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
<span class="add-context-label">添加上下文</span> </svg>
</button> <span class="add-context-label">添加上下文</span>
<span class="tooltiptext">添加文件或代码片段作为上下文</span> <svg class="dropdown-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 714.666667L213.333333 416l42.666667-42.666667L512 629.333333l256-256 42.666667 42.666667z" fill="currentColor"/>
</svg>
</button>
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
</div>
<!-- 上拉菜单 -->
<div class="context-menu" id="contextMenu">
<!-- 主菜单 -->
<div class="context-menu-main" id="contextMenuMain">
<div class="context-menu-item" onclick="showFileList()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326z m1.8 562H232V136h302v216c0 23.2 18.8 42 42 42h216v494z" fill="currentColor"/>
</svg>
<span>文件</span>
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
</svg>
</div>
<div class="context-menu-item" onclick="showFolderList()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z" fill="currentColor"/>
</svg>
<span>文件夹</span>
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
</svg>
</div>
<div class="context-menu-item" onclick="handleAddImage()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
</svg>
<span>图片</span>
</div>
<div class="context-menu-item" onclick="handleAddDocument()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
</svg>
<span>文档库</span>
</div>
</div>
<!-- 文件/文件夹列表视图 -->
<div class="context-menu-list" id="contextMenuList" style="display: none;">
<div class="context-menu-list-header">
<button class="context-menu-back" onclick="backToMainMenu()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8c-16.4 12.8-16.4 37.5 0 50.3l450.8 352.1c5.3 4.1 12.9 0.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z" fill="currentColor"/>
</svg>
</button>
<span id="contextMenuListTitle">选择文件</span>
</div>
<div class="context-menu-list-body" id="contextMenuListBody">
<!-- 动态加载列表 -->
</div>
<div class="context-menu-list-footer">
<input type="text" id="contextMenuSearch" placeholder="搜索..." />
<div class="context-menu-list-actions">
<span id="contextMenuListCount">已选择 0 项</span>
<button class="primary" onclick="confirmSelection()">确定</button>
</div>
</div>
</div>
</div>
</div> </div>
`; `;
} }
@ -24,6 +88,12 @@ export function getContextButtonContent(): string {
*/ */
export function getContextButtonStyles(): string { export function getContextButtonStyles(): string {
return ` return `
/* 上下文选择器容器 */
.context-selector-wrapper {
position: relative;
display: inline-block;
}
/* 添加上下文按钮样式 */ /* 添加上下文按钮样式 */
.add-context-button { .add-context-button {
display: flex; display: flex;
@ -45,15 +115,218 @@ export function getContextButtonStyles(): string {
border-color: var(--vscode-focusBorder); border-color: var(--vscode-focusBorder);
} }
.add-context-button svg { .add-context-button svg.icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
color: #409eff; color: #409eff;
} }
.add-context-button .dropdown-arrow {
width: 12px;
height: 12px;
transition: transform 0.2s ease;
}
.add-context-button.active .dropdown-arrow {
transform: rotate(180deg);
}
.add-context-label { .add-context-label {
white-space: nowrap; white-space: nowrap;
} }
/* 上拉菜单样式 */
.context-menu {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 180px;
z-index: 1000;
display: none;
overflow: hidden;
}
.context-menu.show {
display: block;
animation: slideUp 0.2s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.context-menu-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
cursor: pointer;
transition: background 0.2s ease;
color: var(--vscode-foreground);
}
.context-menu-item:hover {
background: var(--vscode-list-hoverBackground);
}
.context-menu-item svg {
width: 18px;
height: 18px;
flex-shrink: 0;
color: var(--vscode-foreground);
opacity: 0.8;
}
.context-menu-item span {
font-size: 13px;
white-space: nowrap;
flex: 1;
}
.context-menu-item .arrow-right {
width: 14px;
height: 14px;
opacity: 0.6;
margin-left: auto;
}
/* 列表视图样式 */
.context-menu-list {
display: flex;
flex-direction: column;
max-height: 350px;
}
.context-menu-list-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--vscode-panel-border);
}
.context-menu-back {
width: 28px;
height: 28px;
padding: 0;
border: none;
background: transparent;
color: var(--vscode-foreground);
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.context-menu-back:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.context-menu-back svg {
width: 16px;
height: 16px;
}
.context-menu-list-header span {
font-size: 14px;
font-weight: 500;
flex: 1;
}
.context-menu-list-body {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.context-menu-list-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s ease;
}
.context-menu-list-item:hover {
background: var(--vscode-list-hoverBackground);
}
.context-menu-list-item.selected {
background: var(--vscode-list-activeSelectionBackground);
}
.context-menu-list-item input[type="checkbox"] {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.context-menu-list-item label {
flex: 1;
font-size: 12px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-menu-list-footer {
padding: 8px 12px;
border-top: 1px solid var(--vscode-panel-border);
display: flex;
flex-direction: column;
gap: 8px;
}
.context-menu-list-footer input {
width: 100%;
padding: 6px 10px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
color: var(--vscode-input-foreground);
font-size: 12px;
box-sizing: border-box;
}
.context-menu-list-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.context-menu-list-footer span {
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.context-menu-list-footer button {
padding: 4px 12px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.context-menu-list-footer button:hover {
background: var(--vscode-button-hoverBackground);
}
`; `;
} }
@ -62,10 +335,174 @@ export function getContextButtonStyles(): string {
*/ */
export function getContextButtonScript(): string { export function getContextButtonScript(): string {
return ` return `
// 添加上下文处理函数 // 上下文菜单状态
function handleAddContext() { let currentListData = [];
// 发送添加上下文请求到扩展 let currentListType = '';
vscode.postMessage({ command: 'addContext' }); let selectedItems = new Set();
// 切换上下文菜单显示/隐藏
function toggleContextMenu() {
const menu = document.getElementById('contextMenu');
const button = document.querySelector('.add-context-button');
if (menu && button) {
const isShown = menu.classList.contains('show');
if (isShown) {
menu.classList.remove('show');
button.classList.remove('active');
backToMainMenu(); // 关闭时回到主菜单
} else {
menu.classList.add('show');
button.classList.add('active');
}
}
} }
// 点击外部关闭菜单
document.addEventListener('click', function(event) {
const wrapper = document.querySelector('.context-selector-wrapper');
const menu = document.getElementById('contextMenu');
const button = document.querySelector('.add-context-button');
if (wrapper && menu && button && !wrapper.contains(event.target)) {
menu.classList.remove('show');
button.classList.remove('active');
backToMainMenu();
}
});
// 显示文件列表
function showFileList() {
vscode.postMessage({ command: 'addContextFile' });
}
// 显示文件夹列表
function showFolderList() {
vscode.postMessage({ command: 'addContextFolder' });
}
// 返回主菜单
function backToMainMenu() {
const mainMenu = document.getElementById('contextMenuMain');
const listView = document.getElementById('contextMenuList');
if (mainMenu && listView) {
mainMenu.style.display = 'block';
listView.style.display = 'none';
}
selectedItems.clear();
currentListData = [];
}
// 切换到列表视图
function switchToListView(title, type, data) {
const mainMenu = document.getElementById('contextMenuMain');
const listView = document.getElementById('contextMenuList');
const titleEl = document.getElementById('contextMenuListTitle');
if (mainMenu && listView && titleEl) {
mainMenu.style.display = 'none';
listView.style.display = 'flex';
titleEl.textContent = title;
currentListType = type;
currentListData = data;
selectedItems.clear();
renderList(data);
updateSelectedCount();
}
}
// 渲染列表
function renderList(data) {
const body = document.getElementById('contextMenuListBody');
if (!body) return;
body.innerHTML = data.map((item, index) => \`
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
<input type="checkbox" id="item-\${index}" />
<label for="item-\${index}">\${item.relativePath}</label>
</div>
\`).join('');
}
// 切换项选择
function toggleItemSelection(index) {
const checkbox = document.getElementById('item-' + index);
const item = document.querySelectorAll('.context-menu-list-item')[index];
if (checkbox && item) {
checkbox.checked = !checkbox.checked;
if (checkbox.checked) {
selectedItems.add(index);
item.classList.add('selected');
} else {
selectedItems.delete(index);
item.classList.remove('selected');
}
updateSelectedCount();
}
}
// 更新选中数量
function updateSelectedCount() {
const countEl = document.getElementById('contextMenuListCount');
if (countEl) {
countEl.textContent = '已选择 ' + selectedItems.size + ' 项';
}
}
// 确认选择
function confirmSelection() {
const selected = Array.from(selectedItems).map(index => currentListData[index]);
if (selected.length > 0) {
selected.forEach(item => {
addContextItem(currentListType, item.path);
});
}
toggleContextMenu();
}
// 添加图片
function handleAddImage() {
vscode.postMessage({ command: 'addContextImage' });
toggleContextMenu();
}
// 添加文档
function handleAddDocument() {
vscode.postMessage({ command: 'addContextDocument' });
toggleContextMenu();
}
// 搜索功能
const searchInput = document.getElementById('contextMenuSearch');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const keyword = e.target.value.toLowerCase();
const filtered = currentListData.filter(item =>
item.relativePath.toLowerCase().includes(keyword)
);
renderList(filtered);
});
}
// 处理后端消息
window.addEventListener('message', event => {
const message = event.data;
if (message.command === 'showWorkspaceFileList') {
switchToListView('选择文件', 'file', message.files);
} else if (message.command === 'showWorkspaceFolderList') {
switchToListView('选择文件夹', 'folder', message.folders);
}
});
`; `;
} }

225
src/views/contextDisplay.ts Normal file
View File

@ -0,0 +1,225 @@
/**
* 上下文显示组件
* 用于显示已选择的文件、文件夹、图片和文档
*/
/**
* 获取上下文显示区域的 HTML 内容
*/
export function getContextDisplayContent(): string {
return `
<div class="context-display-area" id="contextDisplayArea" style="display: none;">
<div class="context-items-container" id="contextItemsContainer">
<!-- 动态添加的上下文项将显示在这里 -->
</div>
</div>
`;
}
/**
* 获取上下文显示区域的样式
*/
export function getContextDisplayStyles(): string {
return `
/* 上下文显示区域 */
.context-display-area {
margin-bottom: 8px;
padding: 8px;
background: rgba(128, 128, 128, 0.1);
border-radius: 6px;
border: 1px solid var(--vscode-input-border);
}
.context-items-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 上下文项样式 */
.context-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 12px;
color: var(--vscode-foreground);
max-width: 300px;
transition: all 0.2s ease;
}
.context-item:hover {
background: var(--vscode-list-hoverBackground);
}
.context-item svg {
width: 14px;
height: 14px;
flex-shrink: 0;
opacity: 0.8;
}
.context-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-item-remove {
width: 14px;
height: 14px;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s ease;
flex-shrink: 0;
}
.context-item-remove:hover {
opacity: 1;
color: #f56c6c;
}
/* 图片预览样式 */
.context-item.image-item {
position: relative;
}
.context-item-preview {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 3px;
border: 1px solid var(--vscode-input-border);
}
`;
}
/**
* 获取上下文显示区域的脚本
*/
export function getContextDisplayScript(): string {
return `
// 存储上下文项
let contextItems = [];
// 获取文件图标 SVG
function getFileIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
}
// 获取文件夹图标 SVG
function getFolderIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
}
// 获取图片图标 SVG
function getImageIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
}
// 获取文档图标 SVG
function getDocumentIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
}
// 获取删除图标 SVG
function getRemoveIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
}
// 提取文件名
function getFileName(path) {
return path.split(/[\\\\/]/).pop();
}
// 添加上下文项
function addContextItem(type, path) {
const id = Date.now() + Math.random();
contextItems.push({ id, type, path });
renderContextItems();
}
// 删除上下文项
function removeContextItem(id) {
contextItems = contextItems.filter(item => item.id !== id);
renderContextItems();
}
// 渲染上下文项
function renderContextItems() {
const container = document.getElementById('contextItemsContainer');
const displayArea = document.getElementById('contextDisplayArea');
if (!container || !displayArea) return;
if (contextItems.length === 0) {
displayArea.style.display = 'none';
return;
}
displayArea.style.display = 'block';
container.innerHTML = contextItems.map(item => {
let icon = '';
switch(item.type) {
case 'file': icon = getFileIcon(); break;
case 'folder': icon = getFolderIcon(); break;
case 'image': icon = getImageIcon(); break;
case 'document': icon = getDocumentIcon(); break;
}
return \`
<div class="context-item" title="\${item.path}">
\${icon}
<span class="context-item-name">\${getFileName(item.path)}</span>
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
\${getRemoveIcon()}
</span>
</div>
\`;
}).join('');
}
// 处理后端返回的文件选择结果
window.addEventListener('message', event => {
const message = event.data;
switch(message.command) {
case 'contextFilesSelected':
if (message.files && message.files.length > 0) {
message.files.forEach(file => addContextItem('file', file));
}
break;
case 'contextFoldersSelected':
if (message.folders && message.folders.length > 0) {
message.folders.forEach(folder => addContextItem('folder', folder));
}
break;
case 'contextImagesSelected':
if (message.images && message.images.length > 0) {
message.images.forEach(image => addContextItem('image', image));
}
break;
case 'contextDocumentsSelected':
if (message.documents && message.documents.length > 0) {
message.documents.forEach(doc => addContextItem('document', doc));
}
break;
}
});
// 获取所有上下文项(供发送消息时使用)
window.getContextItems = function() {
return contextItems;
};
// 清空上下文项(供清空对话时使用)
window.clearContextItems = function() {
contextItems = [];
renderContextItems();
};
`;
}

View File

@ -14,6 +14,11 @@ import {
getContextButtonStyles, getContextButtonStyles,
getContextButtonScript, getContextButtonScript,
} from "./contextButton"; } from "./contextButton";
import {
getContextDisplayContent,
getContextDisplayStyles,
getContextDisplayScript,
} from "./contextDisplay";
import { import {
getContextCompressContent, getContextCompressContent,
getContextCompressStyles, getContextCompressStyles,
@ -36,13 +41,15 @@ export function getInputAreaContent(
maxIcon: string = '' maxIcon: string = ''
): string { ): string {
return ` return `
<div class="input-area"> <div class="input-area centered" id="inputArea">
<div class="input-group"> <div class="input-group">
<div class="input-wrapper"> <div class="input-wrapper">
<!-- 顶部工具栏 --> <!-- 顶部工具栏 -->
<div class="input-top-toolbar"> <div class="input-top-toolbar">
${getContextButtonContent()} ${getContextButtonContent()}
</div> </div>
<!-- 上下文显示区域 -->
${getContextDisplayContent()}
<textarea <textarea
id="messageInput" id="messageInput"
placeholder="输入您的问题,按 Enter 发送Shift + Enter 换行..." placeholder="输入您的问题,按 Enter 发送Shift + Enter 换行..."
@ -76,12 +83,30 @@ export function getInputAreaStyles(): string {
${getModeSelectorStyles()} ${getModeSelectorStyles()}
${getModelSelectorStyles()} ${getModelSelectorStyles()}
${getContextButtonStyles()} ${getContextButtonStyles()}
${getContextDisplayStyles()}
${getContextCompressStyles()} ${getContextCompressStyles()}
${getOptimizeButtonStyles()} ${getOptimizeButtonStyles()}
.input-area { .input-area {
border-top: 1px solid var(--vscode-panel-border); border-top: 1px solid var(--vscode-panel-border);
padding-top: 15px; padding-top: 15px;
flex-shrink: 0; flex-shrink: 0;
transition: all 0.3s ease;
}
/* 居中模式:未发起对话时 */
.input-area.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(100% - 40px);
max-width: 800px;
border-top: none;
padding-top: 0;
}
/* 底部模式:发起对话后 */
.input-area.bottom {
position: relative;
transform: none;
} }
.input-group { .input-group {
display: flex; display: flex;
@ -264,16 +289,34 @@ export function getInputAreaScript(): string {
// 注意getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载 // 注意getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
${getModelSelectorScript()} ${getModelSelectorScript()}
${getContextButtonScript()} ${getContextButtonScript()}
${getContextDisplayScript()}
${getContextCompressScript()} ${getContextCompressScript()}
${getOptimizeButtonScript()} ${getOptimizeButtonScript()}
// 对话状态管理 // 对话状态管理
let isConversationActive = false; let isConversationActive = false;
let hasMessages = false; // 是否已有消息
// 工作区检测状态 // 工作区检测状态
let hasCheckedWorkspace = false; // 是否已经检测过工作区 let hasCheckedWorkspace = false; // 是否已经检测过工作区
let hasWorkspace = true; // 工作区状态 let hasWorkspace = true; // 工作区状态
// 切换输入框布局模式
function updateInputAreaLayout() {
const inputArea = document.getElementById('inputArea');
if (!inputArea) return;
if (hasMessages) {
// 有消息时,移到底部
inputArea.classList.remove('centered');
inputArea.classList.add('bottom');
} else {
// 无消息时,居中显示
inputArea.classList.add('centered');
inputArea.classList.remove('bottom');
}
}
// 自动调整 textarea 高度 // 自动调整 textarea 高度
function autoResizeTextarea() { function autoResizeTextarea() {
if (messageInput) { if (messageInput) {
@ -357,12 +400,26 @@ export function getInputAreaScript(): string {
const model = getCurrentModel(); // 从模型选择器组件获取当前模型 const model = getCurrentModel(); // 从模型选择器组件获取当前模型
const planMode = document.getElementById('planToggle')?.checked || false; const planMode = document.getElementById('planToggle')?.checked || false;
// 获取上下文项
const contextItems = window.getContextItems ? window.getContextItems() : [];
addMessage(text, 'user'); addMessage(text, 'user');
// 标记已有消息,切换布局到底部
hasMessages = true;
updateInputAreaLayout();
// 切换按钮为暂停状态 // 切换按钮为暂停状态
setSendButtonState(true); setSendButtonState(true);
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model, planMode: planMode }); vscode.postMessage({
command: 'sendMessage',
text: text,
mode: mode,
model: model,
planMode: planMode,
contextItems: contextItems
});
messageInput.value = ''; messageInput.value = '';
autoResizeTextarea(); // 重置输入框高度 autoResizeTextarea(); // 重置输入框高度
messageInput.focus(); messageInput.focus();
@ -370,5 +427,28 @@ export function getInputAreaScript(): string {
// 重置优化状态 // 重置优化状态
resetOptimizeButton(); resetOptimizeButton();
} }
// 全局函数:重置输入框布局(用于清空对话时)
window.resetInputAreaLayout = function() {
hasMessages = false;
updateInputAreaLayout();
};
// 全局函数:检查是否有消息(用于页面加载时)
window.checkMessagesAndUpdateLayout = function() {
const messagesContainer = document.getElementById('messages');
if (messagesContainer) {
const messageElements = messagesContainer.querySelectorAll('.message');
hasMessages = messageElements.length > 0;
updateInputAreaLayout();
}
};
// 页面加载时检查消息状态
setTimeout(() => {
if (window.checkMessagesAndUpdateLayout) {
window.checkMessagesAndUpdateLayout();
}
}, 100);
`; `;
} }

View File

@ -373,6 +373,12 @@ export function getMessageAreaStyles(): string {
margin: 4px 0; margin: 4px 0;
padding: 4px 0; padding: 4px 0;
} }
/* 低调显示的工具调用 - 移除边距和背景 */
.segment-tool.low-profile {
margin: 2px 0;
padding: 0;
background: none;
}
.tool-segment-header { .tool-segment-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -532,6 +538,23 @@ export function getMessageAreaStyles(): string {
.tool-segment-content.collapsed { .tool-segment-content.collapsed {
max-height: 0; max-height: 0;
} }
/* 低调显示的工具调用样式 */
.segment-tool.low-profile .tool-segment-header {
opacity: 0.65;
font-size: 11px;
}
.segment-tool.low-profile .tool-segment-icon {
opacity: 0.55;
font-size: 11px;
}
.segment-tool.low-profile .tool-segment-name {
font-weight: 300;
opacity: 0.8;
}
.segment-tool.low-profile .tool-segment-result {
opacity: 0.7;
font-size: 10px;
}
.segment-question { .segment-question {
background: var(--vscode-textBlockQuote-background); background: var(--vscode-textBlockQuote-background);
border-radius: 6px; border-radius: 6px;
@ -689,8 +712,8 @@ export function getMessageAreaScript(): string {
'queryKnowledgeSummary': '已查询知识摘要', 'queryKnowledgeSummary': '已查询知识摘要',
'queryRules': '已查询规则', 'queryRules': '已查询规则',
'setModule': '已设置模块', 'setModule': '已设置模块',
'addSignal': '已添加信号', 'addSignal': '信号分析完成',
'addSignalExample': '已添加信号示例', 'addSignalExample': '信号示例处理完成',
'validateKnowledgeGraph': '已验证知识图谱', 'validateKnowledgeGraph': '已验证知识图谱',
'querySignals': '已查询信号', 'querySignals': '已查询信号',
'addPlan': '已添加计划', 'addPlan': '已添加计划',
@ -936,8 +959,30 @@ export function getMessageAreaScript(): string {
// 清空容器并重新渲染所有段落 // 清空容器并重新渲染所有段落
currentSegmentedMessage.innerHTML = ''; currentSegmentedMessage.innerHTML = '';
// 合并连续相同的工具调用
const mergedSegments = [];
let i = 0;
while (i < segments.length) {
const segment = segments[i];
if (segment.type === 'tool') {
// 统计连续相同的工具调用
let count = 1;
while (i + count < segments.length &&
segments[i + count].type === 'tool' &&
segments[i + count].toolName === segment.toolName) {
count++;
}
// 添加合并后的段落(带计数)
mergedSegments.push({ ...segment, toolCount: count });
i += count;
} else {
mergedSegments.push(segment);
i++;
}
}
let toolIndex = 0; // 用于跟踪工具段落的索引 let toolIndex = 0; // 用于跟踪工具段落的索引
segments.forEach((segment, index) => { mergedSegments.forEach((segment, index) => {
const segmentDiv = document.createElement('div'); const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type; segmentDiv.className = 'message-segment segment-' + segment.type;
@ -949,8 +994,23 @@ export function getMessageAreaScript(): string {
if (segment.toolName === 'spawnExplorer') { if (segment.toolName === 'spawnExplorer') {
return; return;
} }
// 为技术性工具调用添加低调样式
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
];
if (lowProfileTools.includes(segment.toolName)) {
segmentDiv.className += ' low-profile';
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || ''; const toolResult = segment.toolResult || '';
const toolCount = segment.toolCount || 1;
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
// 检查工具结果是否过长(超过一行显示不下) // 检查工具结果是否过长(超过一行显示不下)
const shouldCollapse = toolResult && toolResult.length > 60; const shouldCollapse = toolResult && toolResult.length > 60;
@ -964,7 +1024,7 @@ export function getMessageAreaScript(): string {
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}"> <div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)} \${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span> <span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''} \${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</div> </div>
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''} \${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
@ -1162,7 +1222,29 @@ export function getMessageAreaScript(): string {
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'message bot-message segmented-message'; container.className = 'message bot-message segmented-message';
segments.forEach((segment, index) => { // 合并连续相同的工具调用
const mergedSegments = [];
let i = 0;
while (i < segments.length) {
const segment = segments[i];
if (segment.type === 'tool') {
// 统计连续相同的工具调用
let count = 1;
while (i + count < segments.length &&
segments[i + count].type === 'tool' &&
segments[i + count].toolName === segment.toolName) {
count++;
}
// 添加合并后的段落(带计数)
mergedSegments.push({ ...segment, toolCount: count });
i += count;
} else {
mergedSegments.push(segment);
i++;
}
}
mergedSegments.forEach((segment, index) => {
const segmentDiv = document.createElement('div'); const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type; segmentDiv.className = 'message-segment segment-' + segment.type;
@ -1174,8 +1256,23 @@ export function getMessageAreaScript(): string {
if (segment.toolName === 'spawnExplorer') { if (segment.toolName === 'spawnExplorer') {
return; return;
} }
// 为技术性工具调用添加低调样式
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
];
if (lowProfileTools.includes(segment.toolName)) {
segmentDiv.className += ' low-profile';
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || ''; const toolResult = segment.toolResult || '';
const toolCount = segment.toolCount || 1;
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
// 检查工具结果是否过长(超过一行显示不下) // 检查工具结果是否过长(超过一行显示不下)
const shouldCollapse = toolResult && toolResult.length > 60; const shouldCollapse = toolResult && toolResult.length > 60;
@ -1183,7 +1280,7 @@ export function getMessageAreaScript(): string {
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}"> <div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)} \${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span> <span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''} \${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</div> </div>
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''} \${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}

View File

@ -6,10 +6,10 @@
* 获取模型选择器的 HTML 内容 * 获取模型选择器的 HTML 内容
*/ */
export function getModelSelectorContent( export function getModelSelectorContent(
autoIcon: string = '', autoIcon: string = "",
liteIcon: string = '', liteIcon: string = "",
syIcon: string = '', syIcon: string = "",
maxIcon: string = '' maxIcon: string = ""
): string { ): string {
return ` return `
<!-- 模型选择 --> <!-- 模型选择 -->
@ -22,25 +22,51 @@ export function getModelSelectorContent(
</svg> </svg>
</div> </div>
<div class="select-dropdown" id="modelDropdown"> <div class="select-dropdown" id="modelDropdown">
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')"> <div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
${autoIcon ? `<img src="${autoIcon}" class="model-icon" alt="Auto">` : ''} ${
<span class="option-label">Auto</span> autoIcon
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
: ""
}
<div class="option-content">
<span class="option-label">Auto</span>
<span class="option-desc">智能匹配最优模型</span>
</div>
</div> </div>
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')"> <div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
${liteIcon ? `<img src="${liteIcon}" class="model-icon" alt="Lite">` : ''} ${
<span class="option-label">Lite</span> liteIcon
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
: ""
}
<div class="option-content">
<span class="option-label">Lite</span>
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
</div>
</div> </div>
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')"> <div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
${syIcon ? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">` : ''} ${
<span class="option-label">Syntaxic</span> syIcon
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
: ""
}
<div class="option-content">
<span class="option-label">Syntaxic</span>
<span class="option-desc">均衡成本和性能节省credits同时保持可靠输出</span>
</div>
</div> </div>
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')"> <div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
${maxIcon ? `<img src="${maxIcon}" class="model-icon" alt="Max">` : ''} ${
<span class="option-label">Max</span> maxIcon
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
: ""
}
<div class="option-content">
<span class="option-label">Max</span>
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
</div>
</div> </div>
</div> </div>
<!-- 模型选择器的 tooltip 容器 -->
<div id="modelTooltip" class="model-tooltip"></div>
</div> </div>
<span class="tooltiptext">选择模型</span> <span class="tooltiptext">选择模型</span>
</div> </div>
@ -104,13 +130,13 @@ export function getModelSelectorStyles(): string {
/* 模型选择器的选项样式 */ /* 模型选择器的选项样式 */
#modelDropdown .select-option { #modelDropdown .select-option {
position: relative; position: relative;
padding: 6px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; transition: background 0.2s ease;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
} }
#modelDropdown .select-option:hover { #modelDropdown .select-option:hover {
background: rgba(128, 128, 128, 0.3); background: rgba(128, 128, 128, 0.3);
@ -125,54 +151,22 @@ export function getModelSelectorStyles(): string {
flex-shrink: 0; flex-shrink: 0;
object-fit: contain; object-fit: contain;
} }
.option-content {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.option-label { .option-label {
font-size: 12px; font-size: 13px;
color: var(--vscode-foreground); color: var(--vscode-foreground);
font-weight: 500;
white-space: nowrap; white-space: nowrap;
} }
/* 模型选择器的 tooltip 样式 */ .option-desc {
.model-tooltip { font-size: 11px;
position: fixed; color: var(--vscode-descriptionForeground);
background: #1e1e1e;
color: #ffffff;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap; white-space: nowrap;
pointer-events: none;
z-index: 10000;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.model-tooltip.show {
opacity: 1;
visibility: visible;
}
/* tooltip 箭头 */
.model-tooltip::before {
content: "";
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
border-width: 7px;
border-style: solid;
border-color: transparent rgba(255, 255, 255, 0.2) transparent transparent;
z-index: -1;
}
.model-tooltip::after {
content: "";
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
border-width: 6px;
border-style: solid;
border-color: transparent #1e1e1e transparent transparent;
margin-right: 1px;
} }
`; `;
} }
@ -235,46 +229,5 @@ export function getModelSelectorScript(): string {
function getCurrentModel() { function getCurrentModel() {
return currentModel; return currentModel;
} }
// 模型选择器 tooltip 功能
(function initModelTooltip() {
const modelDropdown = document.getElementById('modelDropdown');
const modelTooltip = document.getElementById('modelTooltip');
if (!modelDropdown || !modelTooltip) return;
// 为每个选项添加鼠标事件
const options = modelDropdown.querySelectorAll('.select-option');
options.forEach(option => {
option.addEventListener('mouseenter', function(e) {
const tooltipText = this.getAttribute('data-tooltip');
if (!tooltipText) return;
// 设置 tooltip 内容
modelTooltip.textContent = tooltipText;
// 获取选项的位置
const rect = this.getBoundingClientRect();
// 计算 tooltip 位置(在选项右侧)
const tooltipRect = modelTooltip.getBoundingClientRect();
const left = rect.right + 12;
const top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
// 设置位置
modelTooltip.style.left = left + 'px';
modelTooltip.style.top = top + 'px';
// 显示 tooltip
modelTooltip.classList.add('show');
});
option.addEventListener('mouseleave', function() {
// 隐藏 tooltip
modelTooltip.classList.remove('show');
});
});
})();
`; `;
} }

View File

@ -61,10 +61,30 @@ export function getPlanCardStyles(): string {
.plan-step:last-child { .plan-step:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.step-num { .step-checkbox {
color: var(--vscode-textLink-foreground); display: inline-flex;
font-weight: 500; align-items: center;
margin-right: 6px; justify-content: center;
width: 16px;
height: 16px;
margin-right: 8px;
border: 2px solid var(--vscode-textLink-foreground);
border-radius: 4px;
background: transparent;
flex-shrink: 0;
opacity: 0.6;
transition: all 0.2s ease;
}
.step-checkbox.completed {
background: var(--vscode-textLink-foreground);
border-color: var(--vscode-textLink-foreground);
opacity: 1;
}
.step-checkbox.completed::after {
content: '✓';
color: var(--vscode-editor-background);
font-size: 11px;
font-weight: bold;
} }
.plan-actions { .plan-actions {
display: flex; display: flex;
@ -151,7 +171,7 @@ export function getPlanCardScript(): string {
} }
const stepsHtml = (segment.planSteps || []).map((step, i) => const stepsHtml = (segment.planSteps || []).map((step, i) =>
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\` \`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
).join(''); ).join('');
// 选项按钮 // 选项按钮
@ -231,7 +251,7 @@ export function getPlanCardScript(): string {
function renderPlanCardStatic(segment, segmentDiv) { function renderPlanCardStatic(segment, segmentDiv) {
segmentDiv.className += ' segment-plan'; segmentDiv.className += ' segment-plan';
const stepsHtml = (segment.planSteps || []).map((step, i) => const stepsHtml = (segment.planSteps || []).map((step, i) =>
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\` \`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
).join(''); ).join('');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`

411
src/views/progressBar.ts Normal file
View File

@ -0,0 +1,411 @@
/**
* 进度条模块
*
* 功能说明:
* - 显示开发流程进度: Spec -> Design代码编写 -> 仿真检查 -> AST -> Done
* - 支持动态更新当前进度状态
* - 提供视觉反馈显示已完成和进行中的步骤
*/
/**
* 获取进度条的 HTML 内容
*/
export function getProgressBarContent(): string {
return `
<div class="progress-bar-container" style="display: none;">
<div class="progress-bar-header">
<span class="progress-bar-title">开发流程</span>
<button class="progress-bar-toggle" title="收起/展开">
<span class="toggle-icon">▼</span>
</button>
</div>
<div class="progress-steps">
<div class="progress-step" data-step="spec">
<div class="step-circle">
<span class="step-number">1</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Spec设计文档</div>
</div>
<div class="progress-line"></div>
<div class="progress-step" data-step="design">
<div class="step-circle">
<span class="step-number">2</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Design代码编写</div>
</div>
<div class="progress-line"></div>
<div class="progress-step" data-step="simulation">
<div class="step-circle">
<span class="step-number">3</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Sim仿真检查</div>
</div>
<div class="progress-line"></div>
<div class="progress-step" data-step="done">
<div class="step-circle">
<span class="step-number">4</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Done完成</div>
</div>
</div>
</div>
`;
}
/**
* 获取进度条的样式
*/
export function getProgressBarStyles(): string {
return `
.progress-bar-container {
background: var(--vscode-editor-background);
border-bottom: 1px solid var(--vscode-panel-border);
margin-bottom: 5px;
}
.progress-bar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 20px;
cursor: pointer;
user-select: none;
}
.progress-bar-title {
font-size: 11px;
font-weight: 600;
color: var(--vscode-foreground);
}
.progress-bar-toggle {
background: none;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 2px 6px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s;
}
.progress-bar-toggle:hover {
opacity: 1;
}
.toggle-icon {
font-size: 10px;
transition: transform 0.3s ease;
}
.progress-bar-container.collapsed .toggle-icon {
transform: rotate(-90deg);
}
.progress-steps {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 700px;
margin: 0 auto;
padding: 0 20px 10px 20px;
max-height: 60px;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.progress-bar-container.collapsed .progress-steps {
max-height: 0;
padding: 0 20px;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
flex: 0 0 auto;
}
.step-circle {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--vscode-input-background);
border: 2px solid var(--vscode-input-border);
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.3s ease;
z-index: 2;
}
.step-number {
font-size: 10px;
font-weight: 600;
color: var(--vscode-foreground);
}
.step-check {
display: none;
font-size: 12px;
color: var(--vscode-button-foreground);
}
.step-label {
margin-top: 4px;
font-size: 10px;
color: var(--vscode-descriptionForeground);
text-align: center;
white-space: nowrap;
transition: color 0.3s ease;
}
.progress-line {
flex: 1;
height: 2px;
background: var(--vscode-input-border);
margin: 0 6px;
position: relative;
top: -10px;
transition: background 0.3s ease;
}
/* 已完成状态 */
.progress-step.completed .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
}
.progress-step.completed .step-number {
display: none;
}
.progress-step.completed .step-check {
display: block;
}
.progress-step.completed .step-label {
color: var(--vscode-foreground);
font-weight: 500;
}
.progress-step.completed + .progress-line {
background: var(--vscode-button-background);
}
/* 进行中状态 */
.progress-step.active .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
animation: pulse 2s infinite;
}
.progress-step.active .step-number {
color: var(--vscode-button-foreground);
}
.progress-step.active .step-label {
color: var(--vscode-foreground);
font-weight: 600;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
}
50% {
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.progress-steps {
flex-wrap: wrap;
}
.step-label {
font-size: 9px;
}
.step-circle {
width: 20px;
height: 20px;
}
.step-number {
font-size: 9px;
}
.progress-line {
margin: 0 4px;
}
}
`;
}
/**
* 获取进度条的脚本
*/
export function getProgressBarScript(): string {
return `
// 进度条管理
const ProgressBar = {
steps: ['spec', 'design', 'simulation', 'done'],
currentStep: 'spec',
isCollapsed: false,
/**
* 初始化进度条
*/
init() {
this.updateProgress('spec');
this.initToggle();
},
/**
* 初始化收起/展开功能
*/
initToggle() {
const container = document.querySelector('.progress-bar-container');
const header = document.querySelector('.progress-bar-header');
const toggle = document.querySelector('.progress-bar-toggle');
if (!container || !header || !toggle) return;
// 点击头部或按钮都可以切换
const handleToggle = (e) => {
e.stopPropagation();
this.isCollapsed = !this.isCollapsed;
if (this.isCollapsed) {
container.classList.add('collapsed');
} else {
container.classList.remove('collapsed');
}
};
header.addEventListener('click', handleToggle);
toggle.addEventListener('click', handleToggle);
},
/**
* 显示进度条
*/
show() {
const container = document.querySelector('.progress-bar-container');
if (container) {
container.style.display = 'block';
}
},
/**
* 隐藏进度条
*/
hide() {
const container = document.querySelector('.progress-bar-container');
if (container) {
container.style.display = 'none';
}
},
/**
* 更新进度到指定步骤
* @param {string} stepName - 步骤名称
*/
updateProgress(stepName) {
if (!this.steps.includes(stepName)) {
console.warn('Invalid step name:', stepName);
return;
}
this.currentStep = stepName;
const currentIndex = this.steps.indexOf(stepName);
// 更新所有步骤的状态
document.querySelectorAll('.progress-step').forEach((step, index) => {
step.classList.remove('completed', 'active');
if (index < currentIndex) {
step.classList.add('completed');
} else if (index === currentIndex) {
step.classList.add('active');
}
});
// 更新连接线
document.querySelectorAll('.progress-line').forEach((line, index) => {
if (index < currentIndex) {
line.style.background = 'var(--vscode-button-background)';
} else {
line.style.background = 'var(--vscode-input-border)';
}
});
},
/**
* 前进到下一步
*/
nextStep() {
const currentIndex = this.steps.indexOf(this.currentStep);
if (currentIndex < this.steps.length - 1) {
this.updateProgress(this.steps[currentIndex + 1]);
}
},
/**
* 重置进度条
*/
reset() {
this.updateProgress('spec');
},
/**
* 完成所有步骤
*/
complete() {
this.updateProgress('done');
// 将最后一步也标记为完成
const lastStep = document.querySelector('.progress-step[data-step="done"]');
if (lastStep) {
lastStep.classList.remove('active');
lastStep.classList.add('completed');
}
}
};
// 初始化进度条
ProgressBar.init();
// 监听来自扩展的消息以更新进度
window.addEventListener('message', (event) => {
const message = event.data;
if (message.type === 'updateProgress') {
ProgressBar.updateProgress(message.step);
} else if (message.type === 'resetProgress') {
ProgressBar.reset();
} else if (message.type === 'completeProgress') {
ProgressBar.complete();
} else if (message.type === 'showProgress') {
ProgressBar.show();
} else if (message.type === 'hideProgress') {
ProgressBar.hide();
}
});
`;
}

View File

@ -0,0 +1,178 @@
/**
* 思考过程组件
*
* 功能说明:
* - 显示 AI 的思考过程
* - 支持展开/折叠功能
* - 提供打字机效果的流式显示
*/
/**
* 获取思考过程组件的 HTML 内容
* @param thinking - 思考内容
* @param isExpanded - 是否默认展开
*/
export function getThinkingProcessContent(
thinking: string = "",
isExpanded: boolean = false
): string {
return `
<div class="thinking-process-container ${isExpanded ? "expanded" : ""}">
<div class="thinking-header">
<div class="thinking-icon-wrapper">
<svg class="thinking-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<span class="thinking-title">思考过程</span>
</div>
<div class="thinking-content">
<div class="thinking-text">${thinking || "正在思考中..."}</div>
</div>
</div>
`;
}
/**
* 获取思考过程组件的样式
*/
export function getThinkingProcessStyles(): string {
return `
.thinking-process-container {
margin: 12px 0;
border-radius: 6px;
background: var(--vscode-editor-background);
overflow: hidden;
transition: all 0.3s ease;
}
.thinking-header {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 5px;
cursor: pointer;
user-select: none;
background: var(--vscode-input-background);
transition: background 0.2s ease;
width: 85px;
border-radius: 20px;
position: relative;
overflow: hidden;
}
.thinking-header::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
animation: none;
}
.thinking-process-container.typing .thinking-header::before {
animation: shine 2s ease-in-out infinite;
}
@keyframes shine {
0% {
left: -100%;
}
50%, 100% {
left: 100%;
}
}
.thinking-header:hover {
background: var(--vscode-list-hoverBackground);
}
.thinking-icon-wrapper {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.thinking-process-container.expanded .thinking-icon-wrapper {
transform: rotate(90deg);
}
.thinking-icon {
color: var(--vscode-descriptionForeground);
}
.thinking-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: var(--vscode-foreground);
}
.thinking-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
padding: 0 12px;
}
.thinking-process-container.expanded .thinking-content {
max-height: 500px;
padding: 12px;
overflow-y: auto;
}
.thinking-text {
font-size: 12px;
line-height: 1.6;
color: var(--vscode-descriptionForeground);
white-space: pre-wrap;
word-wrap: break-word;
padding-left: 12px;
border-left: 2px solid var(--vscode-textBlockQuote-border);
position: relative;
}
/* 打字机效果 */
.thinking-text.typing::after {
content: '▋';
animation: blink 1s step-end infinite;
margin-left: 2px;
}
@keyframes blink {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0;
}
}
/* 滚动条样式 */
.thinking-content::-webkit-scrollbar {
width: 6px;
}
.thinking-content::-webkit-scrollbar-track {
background: transparent;
}
.thinking-content::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-background);
border-radius: 3px;
}
.thinking-content::-webkit-scrollbar-thumb:hover {
background: var(--vscode-scrollbarSlider-hoverBackground);
}
`;
}

View File

@ -18,6 +18,12 @@ import {
getMessageAreaScript, getMessageAreaScript,
} from "./messageArea"; } from "./messageArea";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard"; import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import {
getProgressBarContent,
getProgressBarStyles,
getProgressBarScript,
} from "./progressBar";
import { getCurrentEnv } from "../config/settings";
/** /**
* 获取 WebView 面板的 HTML 内容 * 获取 WebView 面板的 HTML 内容
*/ */
@ -28,6 +34,10 @@ export function getWebviewContent(
syIconUri?: string, syIconUri?: string,
maxIconUri?: string maxIconUri?: string
): string { ): string {
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
const currentEnv = getCurrentEnv();
const showQuickActions = currentEnv === "dev" || currentEnv === "test";
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
@ -75,6 +85,7 @@ export function getWebviewContent(
${getAgentCardStyles()} ${getAgentCardStyles()}
${getWaveformPreviewContent()} ${getWaveformPreviewContent()}
${getConversationHistoryBarStyles()} ${getConversationHistoryBarStyles()}
${getProgressBarStyles()}
${getInputAreaStyles()} ${getInputAreaStyles()}
.file-editor-section { .file-editor-section {
@ -380,6 +391,7 @@ export function getWebviewContent(
</head> </head>
<body> <body>
${getConversationHistoryBarContent()} ${getConversationHistoryBarContent()}
${getProgressBarContent()}
<div class="header"> <div class="header">
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;"> <div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" /> <img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
@ -397,12 +409,16 @@ export function getWebviewContent(
<span id="statusText">思考中...</span> <span id="statusText">思考中...</span>
</div> </div>
<!-- <div class="quick-actions"> ${
showQuickActions
? `<div class="quick-actions">
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button> <button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button> <button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button> <button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button> <button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
</div> --> </div>`
: ""
}
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)} ${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
</div> </div>
@ -443,10 +459,9 @@ export function getWebviewContent(
} }
if (modeTooltip) { if (modeTooltip) {
const tooltipMap = { const tooltipMap = {
'plan': '只读模式 - 只能查询分析', 'plan': 'plan模式',
'ask': '逐个确认 - 每个写操作需确认', 'ask': 'ask模式',
'agent': '智能体自主模式', 'agent': 'agent模式'
'auto': '完全自动 - 所有操作自动执行'
}; };
modeTooltip.textContent = tooltipMap[value] || '切换模式'; modeTooltip.textContent = tooltipMap[value] || '切换模式';
} }
@ -650,6 +665,10 @@ export function getWebviewContent(
if (messagesContainer) { if (messagesContainer) {
messagesContainer.innerHTML = ''; messagesContainer.innerHTML = '';
} }
// 重置输入框布局到居中
if (typeof window.resetInputAreaLayout === 'function') {
window.resetInputAreaLayout();
}
break; break;
case 'addUserMessage': case 'addUserMessage':
@ -657,6 +676,10 @@ export function getWebviewContent(
if (message.text) { if (message.text) {
addMessage(message.text, 'user'); addMessage(message.text, 'user');
} }
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
break; break;
case 'addAiMessage': case 'addAiMessage':
@ -664,6 +687,10 @@ export function getWebviewContent(
if (message.text) { if (message.text) {
addMessage(message.text, 'bot'); addMessage(message.text, 'bot');
} }
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
break; break;
case 'switchMode': case 'switchMode':
@ -696,6 +723,7 @@ export function getWebviewContent(
${getAgentCardScript()} ${getAgentCardScript()}
${getWaveformPreviewScript()} ${getWaveformPreviewScript()}
${getConversationHistoryBarScript()} ${getConversationHistoryBarScript()}
${getProgressBarScript()}
${getInputAreaScript()} ${getInputAreaScript()}
</script></body> </script></body>
</html>`; </html>`;