Compare commits
44 Commits
16e91bd2c0
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
| cca82c7885 | |||
| 3831de2849 | |||
| 0df529c4fd | |||
| 5c53d7f0e9 | |||
| ef2a0dc16e | |||
| 5ce420295b | |||
| 1d7f3d7626 | |||
| 9b0d2d5e01 | |||
| 27e3351b55 | |||
| de3e84aa4e | |||
| 8dc34ee435 | |||
| d8cd86361e | |||
| acf3f9ff37 | |||
| c27b08cccf | |||
| 9fc3c9f056 | |||
| 60d8eaf0eb | |||
| df6f983e83 | |||
| acf60f2a17 | |||
| f933d84cd1 | |||
| b794d1ceb0 | |||
| 259310a29d | |||
| 715eac5949 | |||
| c2936395d9 | |||
| 8762eacb3e | |||
| 3d535fd3e1 | |||
| ecdbe0bdc0 | |||
| c49aaf753c | |||
| 0f8674e1c7 | |||
| ef2159f1bd | |||
| b662d25c9c | |||
| 1ce1ed715c | |||
| 2587018405 | |||
| 28b75e8475 | |||
| 5287d483d8 | |||
| e6b6bc3698 | |||
| d43cd610a0 | |||
| 842e5fb49b | |||
| 430581598b | |||
| 2d5b297171 | |||
| 02b56a7031 | |||
| c42ebdfe59 | |||
| 3f0cc8ae29 | |||
| 0f458f6299 | |||
| d415d8ee4e |
28
package.json
28
package.json
@ -9,7 +9,8 @@
|
|||||||
},
|
},
|
||||||
"icon": "media/icon.png",
|
"icon": "media/icon.png",
|
||||||
"categories": [
|
"categories": [
|
||||||
"Other"
|
"Chat",
|
||||||
|
"Programming Languages"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"IC",
|
"IC",
|
||||||
@ -69,27 +70,7 @@
|
|||||||
"id": "iccoder",
|
"id": "iccoder",
|
||||||
"label": "IC Coder"
|
"label": "IC Coder"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"configuration": {
|
|
||||||
"title": "IC Coder",
|
|
||||||
"properties": {
|
|
||||||
"icCoder.backendUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "http://localhost:2233",
|
|
||||||
"description": "后端服务地址"
|
|
||||||
},
|
|
||||||
"icCoder.timeout": {
|
|
||||||
"type": "number",
|
|
||||||
"default": 60000,
|
|
||||||
"description": "请求超时时间(毫秒)"
|
|
||||||
},
|
|
||||||
"icCoder.userId": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "default-user",
|
|
||||||
"description": "用户ID(临时配置)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vscode:prepublish": "pnpm run package",
|
"vscode:prepublish": "pnpm run package",
|
||||||
@ -120,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",
|
||||||
|
|||||||
BIN
src/assets/model/Auto.png
Normal file
BIN
src/assets/model/Auto.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/model/Max.png
Normal file
BIN
src/assets/model/Max.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/model/Sy.png
Normal file
BIN
src/assets/model/Sy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/model/lite.png
Normal file
BIN
src/assets/model/lite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@ -1,9 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* 配置管理
|
* 配置管理
|
||||||
* 从 VSCode 设置读取配置项
|
* 支持 dev(本地开发)和 test(测试服务器)两种环境
|
||||||
*/
|
*/
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
/** 环境类型 */
|
||||||
|
type Environment = "dev" | "test" | "prod";
|
||||||
|
|
||||||
|
/** 当前环境 - 修改这里切换环境 */
|
||||||
|
const CURRENT_ENV: Environment = "test";
|
||||||
|
|
||||||
/** 配置项接口 */
|
/** 配置项接口 */
|
||||||
export interface IccoderConfig {
|
export interface IccoderConfig {
|
||||||
/** 后端服务地址 */
|
/** 后端服务地址 */
|
||||||
@ -14,24 +20,40 @@ export interface IccoderConfig {
|
|||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 默认配置 */
|
/** 环境配置 */
|
||||||
const DEFAULT_CONFIG: IccoderConfig = {
|
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||||
backendUrl: "http://localhost:8080",
|
/** 本地开发环境 */
|
||||||
timeout: 3600000, // 1小时
|
dev: {
|
||||||
userId: "default-user",
|
backendUrl: "http://localhost:2233",
|
||||||
|
timeout: 60000,
|
||||||
|
userId: "default-user",
|
||||||
|
},
|
||||||
|
/** 测试服务器环境 */
|
||||||
|
test: {
|
||||||
|
backendUrl: "http://192.168.1.108:2233",
|
||||||
|
timeout: 60000,
|
||||||
|
userId: "default-user",
|
||||||
|
},
|
||||||
|
/** 生产环境 */
|
||||||
|
prod: {
|
||||||
|
backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址
|
||||||
|
timeout: 60000,
|
||||||
|
userId: "default-user",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前环境
|
||||||
|
*/
|
||||||
|
export function getCurrentEnv(): Environment {
|
||||||
|
return CURRENT_ENV;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取配置项
|
* 获取配置项
|
||||||
*/
|
*/
|
||||||
export function getConfig(): IccoderConfig {
|
export function getConfig(): IccoderConfig {
|
||||||
const config = vscode.workspace.getConfiguration("icCoder");
|
return { ...ENV_CONFIG[CURRENT_ENV] };
|
||||||
|
|
||||||
return {
|
|
||||||
backendUrl: config.get<string>("backendUrl", DEFAULT_CONFIG.backendUrl),
|
|
||||||
timeout: config.get<number>("timeout", DEFAULT_CONFIG.timeout),
|
|
||||||
userId: config.get<string>("userId", DEFAULT_CONFIG.userId),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +61,6 @@ export function getConfig(): IccoderConfig {
|
|||||||
*/
|
*/
|
||||||
export function getApiUrl(path: string): string {
|
export function getApiUrl(path: string): string {
|
||||||
const { backendUrl } = getConfig();
|
const { backendUrl } = getConfig();
|
||||||
// 确保 URL 格式正确
|
|
||||||
const baseUrl = backendUrl.endsWith("/")
|
const baseUrl = backendUrl.endsWith("/")
|
||||||
? backendUrl.slice(0, -1)
|
? backendUrl.slice(0, -1)
|
||||||
: backendUrl;
|
: backendUrl;
|
||||||
|
|||||||
@ -70,3 +70,103 @@ export const stopIconSvg = `
|
|||||||
<path d="M349.75 349.75m57.15 0l210.2 0q57.15 0 57.15 57.15l0 210.2q0 57.15-57.15 57.15l-210.2 0q-57.15 0-57.15-57.15l0-210.2q0-57.15 57.15-57.15Z" fill="currentColor"></path>
|
<path d="M349.75 349.75m57.15 0l210.2 0q57.15 0 57.15 57.15l0 210.2q0 57.15-57.15 57.15l-210.2 0q-57.15 0-57.15-57.15l0-210.2q0-57.15 57.15-57.15Z" fill="currentColor"></path>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 探索智能体图标 SVG
|
||||||
|
*/
|
||||||
|
export const agentIconSvg = `
|
||||||
|
<svg t="1767101071638" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7779" width="16" height="16" style="display: inline-block; vertical-align: middle;"><path d="M173.474909 410.414545c-20.293818 0-33.838545-13.498182-33.838545-33.792v-135.377454C139.636364 187.066182 187.019636 139.636364 241.198545 139.636364h135.447273c20.270545 0 33.815273 13.591273 33.815273 33.885091s-13.591273 33.838545-33.885091 33.838545h-135.447273c-20.317091 0-33.815273 13.591273-33.815272 33.885091v135.377454c0.046545 20.293818-13.498182 33.792-33.838546 33.792z m203.101091 473.902546h-135.447273C187.042909 884.317091 139.636364 836.933818 139.636364 782.754909V647.447273c0-20.386909 13.544727-33.838545 33.838545-33.838546s33.885091 13.451636 33.885091 33.838546v135.330909c0 20.293818 13.544727 33.931636 33.838545 33.931636h135.447273c20.270545 0 33.815273 13.451636 33.815273 33.745455-0.046545 20.340364-13.591273 33.885091-33.885091 33.885091z m406.178909 0H647.447273c-20.386909 0-33.931636-13.544727-33.931637-33.931636 0-20.293818 13.544727-33.745455 33.931637-33.745455h135.330909c20.386909 0 33.838545-13.637818 33.838545-33.931636V647.447273c0-20.386909 13.544727-33.838545 33.931637-33.838546 20.293818 0 33.838545 13.451636 33.838545 33.838546v135.330909c-0.046545 54.178909-47.522909 101.562182-101.608727 101.562182z m67.723636-473.902546c-20.386909 0-33.931636-13.498182-33.931636-33.792v-135.377454c0-20.340364-13.451636-33.885091-33.838545-33.885091H647.447273c-20.386909 0-33.931636-13.498182-33.931637-33.838545S627.083636 139.636364 647.424 139.636364h135.330909c54.085818 0 101.562182 47.429818 101.562182 101.608727v135.377454c0 20.293818-13.544727 33.792-33.838546 33.792z m0 135.493819H173.474909c-20.293818 0-33.838545-13.591273-33.838545-33.931637s13.544727-33.885091 33.838545-33.885091h677.003636c20.293818 0 33.838545 13.591273 33.838546 33.885091s-13.544727 33.931636-33.838546 33.931637z" fill="#8a8a8a" p-id="7780"></path></svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* planner 图标 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
|
||||||
|
*/
|
||||||
|
export const saveKnowledgeIconSvg = `
|
||||||
|
<span class="tool-save-knowledge-icon">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M827.733333 315.733333l-123.733333-123.733333c-12.8-12.8-34.133333-21.333333-59.733333-21.333334H256c-46.96 0-85.44 40.96-85.44 85.44v512c0 46.96 40.96 85.44 85.44 85.44h512c46.96 0 85.44-40.96 85.44-85.44V375.466667c0-25.6-8.533333-46.933333-21.333333-59.733334z m-140.8 469.333334H337.066667v-85.333334h349.866666v85.333334z m0-170.666667H337.066667v-85.333333h349.866666v85.333333z m0-170.666667H337.066667v-85.333333h349.866666v85.333333z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件读取图标 SVG
|
||||||
|
*/
|
||||||
|
export const fileReadIconSvg = `
|
||||||
|
<span class="tool-file-read-icon">
|
||||||
|
<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="#8a8a8a"/>
|
||||||
|
<path d="M342 472h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM342 616h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM342 760h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件删除图标 SVG
|
||||||
|
*/
|
||||||
|
export const fileDeleteIconSvg = `
|
||||||
|
<span class="tool-file-delete-icon">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72z" fill="#8a8a8a"/>
|
||||||
|
<path d="M832 256H192c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仿真图标 SVG
|
||||||
|
*/
|
||||||
|
export const simulationIconSvg = `
|
||||||
|
<span class="tool-simulation-icon">
|
||||||
|
<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" fill="#8a8a8a"/>
|
||||||
|
<path d="M210 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H210c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM210 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H210c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8zM462 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H462c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM462 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H462c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8zM714 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H714c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM714 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H714c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 波形分析图标 SVG
|
||||||
|
*/
|
||||||
|
export const waveformIconSvg = `
|
||||||
|
<span class="tool-waveform-icon">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M952 474H829.8C812.5 327.6 696.4 211.5 550 194.2V72c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v122.2C327.6 211.5 211.5 327.6 194.2 474H72c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h122.2C211.5 696.4 327.6 812.5 474 829.8V952c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V829.8C696.4 812.5 812.5 696.4 829.8 550H952c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zM512 756c-134.8 0-244-109.2-244-244s109.2-244 244-244 244 109.2 244 244-109.2 244-244 244z" fill="#8a8a8a"/>
|
||||||
|
<path d="M512 392c-32.1 0-62.1 12.4-84.8 35.2-22.7 22.7-35.2 52.7-35.2 84.8s12.5 62.1 35.2 84.8c22.7 22.7 52.7 35.2 84.8 35.2s62.1-12.5 84.8-35.2c22.7-22.7 35.2-52.7 35.2-84.8s-12.5-62.1-35.2-84.8C574.1 404.4 544.1 392 512 392z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库加载图标 SVG
|
||||||
|
*/
|
||||||
|
export const knowledgeLoadIconSvg = `
|
||||||
|
<span class="tool-knowledge-load-icon">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" fill="#8a8a8a"/>
|
||||||
|
<path d="M492 400h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM492 544h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM492 688h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM340 368a40 40 0 1 0 80 0 40 40 0 1 0-80 0zM340 512a40 40 0 1 0 80 0 40 40 0 1 0-80 0zM340 656a40 40 0 1 0 80 0 40 40 0 1 0-80 0z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态转换图标 SVG
|
||||||
|
*/
|
||||||
|
export const stateTransitionIconSvg = `
|
||||||
|
<span class="tool-state-transition-icon">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<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="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.9-11.2z" fill="#8a8a8a"/>
|
||||||
|
<path d="M512 320c-4.4 0-8 3.6-8 8v184c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H560V328c0-4.4-3.6-8-8-8h-40z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|||||||
@ -12,7 +12,9 @@ import {
|
|||||||
handlePlanAction,
|
handlePlanAction,
|
||||||
setPendingPlanExecution,
|
setPendingPlanExecution,
|
||||||
getCurrentTaskId,
|
getCurrentTaskId,
|
||||||
|
setLastTaskId,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
|
import { compactDialog } from "../services/apiClient";
|
||||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||||
import { MessageType } from "../types/chatHistory";
|
import { MessageType } from "../types/chatHistory";
|
||||||
@ -26,21 +28,27 @@ export async function showICHelperPanel(
|
|||||||
) {
|
) {
|
||||||
// 检查用户是否已登录
|
// 检查用户是否已登录
|
||||||
try {
|
try {
|
||||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
createIfNone: false,
|
||||||
|
});
|
||||||
if (!session) {
|
if (!session) {
|
||||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
vscode.window
|
||||||
|
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||||
|
.then((selection) => {
|
||||||
|
if (selection === "立即登录") {
|
||||||
|
vscode.commands.executeCommand("ic-coder.login");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window
|
||||||
|
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||||
|
.then((selection) => {
|
||||||
if (selection === "立即登录") {
|
if (selection === "立即登录") {
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
vscode.commands.executeCommand("ic-coder.login");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
|
||||||
if (selection === "立即登录") {
|
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +60,10 @@ export async function showICHelperPanel(
|
|||||||
{
|
{
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
localResourceRoots: [
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||||
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -74,8 +85,28 @@ export async function showICHelperPanel(
|
|||||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 获取模型图标URI
|
||||||
|
const autoIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||||
|
);
|
||||||
|
const liteIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||||
|
);
|
||||||
|
const syIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||||
|
);
|
||||||
|
const maxIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||||
|
);
|
||||||
|
|
||||||
// 设置HTML内容
|
// 设置HTML内容
|
||||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
panel.webview.html = getWebviewContent(
|
||||||
|
iconUri.toString(),
|
||||||
|
autoIconUri.toString(),
|
||||||
|
liteIconUri.toString(),
|
||||||
|
syIconUri.toString(),
|
||||||
|
maxIconUri.toString()
|
||||||
|
);
|
||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
panel.webview.onDidReceiveMessage(
|
panel.webview.onDidReceiveMessage(
|
||||||
@ -110,7 +141,15 @@ export async function showICHelperPanel(
|
|||||||
// 切换到当前面板的任务上下文
|
// 切换到当前面板的任务上下文
|
||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
// 显示进度条
|
||||||
|
panel.webview.postMessage({ type: 'showProgress' });
|
||||||
|
|
||||||
|
handleUserMessage(
|
||||||
|
panel,
|
||||||
|
message.text,
|
||||||
|
context.extensionPath,
|
||||||
|
message.mode
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
handleReadFile(panel, message.filePath);
|
handleReadFile(panel, message.filePath);
|
||||||
@ -182,26 +221,194 @@ export async function showICHelperPanel(
|
|||||||
break;
|
break;
|
||||||
// 新增:中止对话
|
// 新增:中止对话
|
||||||
case "abortDialog":
|
case "abortDialog":
|
||||||
abortCurrentDialog();
|
void abortCurrentDialog();
|
||||||
|
break;
|
||||||
|
// 新增:压缩会话
|
||||||
|
case "compressConversation":
|
||||||
|
{
|
||||||
|
const taskId = getCurrentTaskId();
|
||||||
|
if (taskId) {
|
||||||
|
compactDialog(taskId)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.success) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveMessage",
|
||||||
|
text: "✅ 会话压缩完成",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveMessage",
|
||||||
|
text: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveMessage",
|
||||||
|
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveMessage",
|
||||||
|
text: "❌ 没有活跃的会话",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||||
case "planAction":
|
case "planAction":
|
||||||
if (message.action === 'confirm') {
|
if (message.action === "confirm") {
|
||||||
// 确认执行:切换到 Agent 模式
|
// 确认执行:切换到 Agent 模式
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: 'switchMode',
|
command: "switchMode",
|
||||||
mode: 'agent'
|
mode: "agent",
|
||||||
});
|
});
|
||||||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||||||
const taskId = getCurrentTaskId();
|
const taskId = getCurrentTaskId();
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||||||
setPendingPlanExecution(panel, message.planTitle || '计划', context.extensionPath, taskId);
|
setPendingPlanExecution(
|
||||||
|
panel,
|
||||||
|
message.planTitle || "计划",
|
||||||
|
context.extensionPath,
|
||||||
|
taskId
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.warn('[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失');
|
console.warn(
|
||||||
|
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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":
|
||||||
|
const hasWorkspace = !!(
|
||||||
|
vscode.workspace.workspaceFolders &&
|
||||||
|
vscode.workspace.workspaceFolders.length > 0
|
||||||
|
);
|
||||||
|
if (!hasWorkspace) {
|
||||||
|
// 弹窗提示用户需要打开工作区
|
||||||
|
vscode.window
|
||||||
|
.showWarningMessage(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||||
|
"打开文件夹"
|
||||||
|
)
|
||||||
|
.then((selection) => {
|
||||||
|
if (selection === "打开文件夹") {
|
||||||
|
vscode.commands.executeCommand("vscode.openFolder");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 返回工作区状态给前端
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "workspaceStatus",
|
||||||
|
hasWorkspace: hasWorkspace,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@ -485,6 +692,9 @@ async function selectConversation(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置 lastTaskId,用于压缩等操作
|
||||||
|
setLastTaskId(taskId);
|
||||||
|
|
||||||
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
||||||
const panelId = (panel as any).__uniqueId;
|
const panelId = (panel as any).__uniqueId;
|
||||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||||
|
|||||||
@ -126,6 +126,55 @@ export async function healthCheck(): Promise<{ status: string }> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止对话请求
|
||||||
|
*/
|
||||||
|
export interface StopDialogRequest {
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止对话响应
|
||||||
|
*/
|
||||||
|
export interface StopDialogResponse {
|
||||||
|
success: boolean;
|
||||||
|
taskId: string;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止对话
|
||||||
|
* POST /api/dialog/stop
|
||||||
|
*/
|
||||||
|
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
|
||||||
|
console.log(`[API] 停止对话: taskId=${taskId}`);
|
||||||
|
return request<StopDialogResponse>('/api/dialog/stop', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { taskId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 压缩对话响应 */
|
||||||
|
export interface CompactDialogResponse {
|
||||||
|
success: boolean;
|
||||||
|
taskId: string;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动压缩对话历史
|
||||||
|
* POST /api/dialog/compact
|
||||||
|
*/
|
||||||
|
export async function compactDialog(taskId: string): Promise<CompactDialogResponse> {
|
||||||
|
console.log(`[API] 压缩对话: taskId=${taskId}`);
|
||||||
|
return request<CompactDialogResponse>('/api/dialog/compact', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { taskId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建成功的工具结果
|
* 创建成功的工具结果
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from
|
|||||||
import { userInteractionManager } from './userInteraction';
|
import { userInteractionManager } from './userInteraction';
|
||||||
import { getConfig } from '../config/settings';
|
import { getConfig } from '../config/settings';
|
||||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||||
import { submitToolConfirm, submitAnswer } from './apiClient';
|
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
|
||||||
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,6 +73,8 @@ export interface DialogCallbacks {
|
|||||||
onError?: (message: string) => void;
|
onError?: (message: string) => void;
|
||||||
/** 通知消息 */
|
/** 通知消息 */
|
||||||
onNotification?: (message: string) => void;
|
onNotification?: (message: string) => void;
|
||||||
|
/** 上下文使用量更新 */
|
||||||
|
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,30 +161,111 @@ export class DialogSession {
|
|||||||
* 加载知识图谱数据
|
* 加载知识图谱数据
|
||||||
* 从 .iccoder/knowledge.json 读取
|
* 从 .iccoder/knowledge.json 读取
|
||||||
*/
|
*/
|
||||||
private loadKnowledgeData(): string | null {
|
private async loadKnowledgeData(): Promise<string | null> {
|
||||||
console.log('[DialogSession] loadKnowledgeData 开始执行');
|
console.log('[DialogSession] loadKnowledgeData 开始执行');
|
||||||
|
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
// 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空)
|
||||||
|
const workspaceFolders = await this.waitForWorkspaceFolders();
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
console.log('[DialogSession] 没有工作区文件夹');
|
console.log('[DialogSession] 没有工作区文件夹');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
// 多根工作区场景:优先读取实际存在 knowledge.json 的根目录
|
||||||
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json');
|
for (const folder of this.getWorkspaceFolderCandidates(workspaceFolders)) {
|
||||||
console.log('[DialogSession] 知识图谱路径:', knowledgePath);
|
const knowledgeUri = vscode.Uri.joinPath(folder.uri, '.iccoder', 'knowledge.json');
|
||||||
|
console.log('[DialogSession] 知识图谱 URI:', knowledgeUri.toString());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exists = fs.existsSync(knowledgePath);
|
const content = await this.readTextFileWithRetry(knowledgeUri, 5);
|
||||||
console.log('[DialogSession] 文件存在:', exists);
|
if (!content) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (exists) {
|
// 基础校验 + 清洗:避免偶发读取到半截内容导致后端反序列化失败
|
||||||
const content = fs.readFileSync(knowledgePath, 'utf-8');
|
try {
|
||||||
console.log('[DialogSession] 加载知识图谱成功, 长度:', content.length);
|
const parsed = JSON.parse(content) as any;
|
||||||
return content;
|
|
||||||
|
// 兼容:后端 KnowledgeGraph.isEmpty() 可能被序列化为 "empty",老后端反序列化会失败
|
||||||
|
if (parsed && typeof parsed === 'object' && 'empty' in parsed) {
|
||||||
|
delete parsed.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = JSON.stringify(parsed);
|
||||||
|
console.log('[DialogSession] 知识图谱已清洗, sanitizedLen:', sanitized.length);
|
||||||
|
return sanitized;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[DialogSession] 知识图谱 JSON 解析失败,跳过本次读取:', e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[DialogSession] 加载知识图谱失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForWorkspaceFolders(): Promise<readonly vscode.WorkspaceFolder[] | undefined> {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const folders = vscode.workspace.workspaceFolders;
|
||||||
|
if (folders && folders.length > 0) {
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
return vscode.workspace.workspaceFolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWorkspaceFolderCandidates(
|
||||||
|
workspaceFolders: readonly vscode.WorkspaceFolder[]
|
||||||
|
): vscode.WorkspaceFolder[] {
|
||||||
|
const result: vscode.WorkspaceFolder[] = [];
|
||||||
|
|
||||||
|
// 1) 当前激活文件所在的 workspace folder(如果有)
|
||||||
|
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||||
|
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||||
|
if (activeFolder) {
|
||||||
|
result.push(activeFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 其它 workspace folders(去重)
|
||||||
|
for (const folder of workspaceFolders) {
|
||||||
|
if (!result.some(f => f.uri.toString() === folder.uri.toString())) {
|
||||||
|
result.push(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readTextFileWithRetry(uri: vscode.Uri, maxAttempts: number): Promise<string | null> {
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
const bytes = await vscode.workspace.fs.readFile(uri);
|
||||||
|
const text = Buffer.from(bytes).toString('utf-8');
|
||||||
|
if (!text || !text.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
} catch (error) {
|
||||||
|
// 文件不存在:不是错误,直接返回 null
|
||||||
|
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryable =
|
||||||
|
(error instanceof vscode.FileSystemError && error.code === 'Unavailable') ||
|
||||||
|
(typeof (error as any)?.code === 'string' && ['EBUSY', 'EPERM', 'EACCES'].includes((error as any).code));
|
||||||
|
|
||||||
|
if (!retryable || attempt >= maxAttempts) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayMs = 50 * attempt;
|
||||||
|
console.log(`[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.warn('[DialogSession] 加载知识图谱失败:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -253,7 +336,7 @@ export class DialogSession {
|
|||||||
const newMessages = historyManager.getNewMessagesSinceCompaction();
|
const newMessages = historyManager.getNewMessagesSinceCompaction();
|
||||||
|
|
||||||
// 加载知识图谱数据
|
// 加载知识图谱数据
|
||||||
const knowledgeData = this.loadKnowledgeData();
|
const knowledgeData = await this.loadKnowledgeData();
|
||||||
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
|
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
|
||||||
|
|
||||||
const request: DialogRequest = {
|
const request: DialogRequest = {
|
||||||
@ -353,30 +436,64 @@ export class DialogSession {
|
|||||||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||||||
console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId);
|
console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId);
|
||||||
|
|
||||||
// 调用回调通知 UI 显示确认对话框
|
// 结束当前文本段落
|
||||||
|
this.finalizeTextSegment();
|
||||||
|
|
||||||
|
// 生成工具描述
|
||||||
|
const toolDescription = this.getToolDescription(data.toolName, data.toolInput);
|
||||||
|
|
||||||
|
// 构建问题文本
|
||||||
|
const toolNameMap: Record<string, string> = {
|
||||||
|
'file_write': '写入文件',
|
||||||
|
'file_delete': '删除文件',
|
||||||
|
'syntax_check': '语法检查',
|
||||||
|
'simulation': '运行仿真'
|
||||||
|
};
|
||||||
|
const toolDisplayName = toolNameMap[data.toolName] || data.toolName;
|
||||||
|
const question = `确认执行操作:${toolDisplayName}\n\n${toolDescription}`;
|
||||||
|
|
||||||
|
// 生成唯一的 askId
|
||||||
|
const askId = `tool_confirm_${data.confirmId}`;
|
||||||
|
|
||||||
|
// 添加问题段落到聊天界面
|
||||||
|
this.segments.push({
|
||||||
|
type: 'question',
|
||||||
|
askId: askId,
|
||||||
|
question: question,
|
||||||
|
options: ['确认执行', '取消']
|
||||||
|
});
|
||||||
|
|
||||||
|
// 实时发送段落更新
|
||||||
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
|
||||||
|
// 调用回调通知 UI
|
||||||
callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput);
|
callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput);
|
||||||
|
|
||||||
// 使用 VSCode 快速选择框显示确认对话框
|
// 使用 userInteractionManager 等待用户回答
|
||||||
const toolDescription = this.getToolDescription(data.toolName, data.toolInput);
|
|
||||||
const result = await vscode.window.showWarningMessage(
|
|
||||||
`确认执行操作: ${data.toolName}`,
|
|
||||||
{ modal: true, detail: toolDescription },
|
|
||||||
'确认执行',
|
|
||||||
'取消'
|
|
||||||
);
|
|
||||||
|
|
||||||
const approved = result === '确认执行';
|
|
||||||
console.log('[DialogSession] 用户确认结果:', approved);
|
|
||||||
|
|
||||||
// 发送确认响应到后端
|
|
||||||
try {
|
try {
|
||||||
await submitToolConfirm({
|
await userInteractionManager.handleAskUser(
|
||||||
confirmId: data.confirmId,
|
{
|
||||||
taskId: this.taskId,
|
askId: askId,
|
||||||
approved
|
question: question,
|
||||||
});
|
options: ['确认执行', '取消']
|
||||||
|
} as AskUserEvent,
|
||||||
|
this.taskId
|
||||||
|
);
|
||||||
|
|
||||||
|
// 注意:用户回答后,需要在 receiveAnswer 中处理 tool_confirm 类型的 askId
|
||||||
|
// 这里不直接调用 submitToolConfirm,而是在 userInteractionManager 中统一处理
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[DialogSession] 发送确认响应失败:', error);
|
console.error('[DialogSession] 处理工具确认失败:', error);
|
||||||
|
// 如果出错,默认取消执行
|
||||||
|
try {
|
||||||
|
await submitToolConfirm({
|
||||||
|
confirmId: data.confirmId,
|
||||||
|
taskId: this.taskId,
|
||||||
|
approved: false
|
||||||
|
});
|
||||||
|
} catch (submitError) {
|
||||||
|
console.error('[DialogSession] 发送取消响应失败:', submitError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -438,6 +555,12 @@ export class DialogSession {
|
|||||||
onComplete: (data) => {
|
onComplete: (data) => {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.finalizeTextSegment();
|
this.finalizeTextSegment();
|
||||||
|
|
||||||
|
// 追踪 AI 消息(用于后端重启后恢复)
|
||||||
|
if (this.accumulatedText) {
|
||||||
|
historyManager.trackAiMessage(this.accumulatedText);
|
||||||
|
}
|
||||||
|
|
||||||
// 发送所有段落
|
// 发送所有段落
|
||||||
callbacks.onComplete?.(this.segments);
|
callbacks.onComplete?.(this.segments);
|
||||||
},
|
},
|
||||||
@ -524,6 +647,11 @@ export class DialogSession {
|
|||||||
await historyManager.saveCompactedData(data.compactedData);
|
await historyManager.saveCompactedData(data.compactedData);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onContextUsage: (data) => {
|
||||||
|
console.log('[DialogSession] onContextUsage:', data.currentTokens, '/', data.maxTokens);
|
||||||
|
callbacks.onContextUsage?.(data);
|
||||||
|
},
|
||||||
|
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
console.log('[DialogSession] SSE 连接已建立');
|
console.log('[DialogSession] SSE 连接已建立');
|
||||||
},
|
},
|
||||||
@ -554,6 +682,25 @@ export class DialogSession {
|
|||||||
}
|
}
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
userInteractionManager.cancelAll();
|
userInteractionManager.cancelAll();
|
||||||
|
|
||||||
|
// 通知后端停止处理
|
||||||
|
stopDialog(this.taskId).catch(err => {
|
||||||
|
console.warn('[DialogSession] 停止对话请求失败:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前的消息段落(用于中止时保存)
|
||||||
|
*/
|
||||||
|
getSegments(): MessageSegment[] {
|
||||||
|
return this.segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取累积的文本内容
|
||||||
|
*/
|
||||||
|
getAccumulatedText(): string {
|
||||||
|
return this.accumulatedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -27,7 +27,8 @@ import type {
|
|||||||
AgentStartEvent,
|
AgentStartEvent,
|
||||||
AgentProgressEvent,
|
AgentProgressEvent,
|
||||||
AgentCompleteEvent,
|
AgentCompleteEvent,
|
||||||
AgentErrorEvent
|
AgentErrorEvent,
|
||||||
|
ContextUsageEvent
|
||||||
} from '../types/api';
|
} from '../types/api';
|
||||||
import type { MemoryCompactedEvent } from '../types/memory';
|
import type { MemoryCompactedEvent } from '../types/memory';
|
||||||
|
|
||||||
@ -71,6 +72,8 @@ export interface SSECallbacks {
|
|||||||
onAgentError?: (data: AgentErrorEvent) => void;
|
onAgentError?: (data: AgentErrorEvent) => void;
|
||||||
/** 记忆压缩完成 */
|
/** 记忆压缩完成 */
|
||||||
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
||||||
|
/** 上下文使用量更新 */
|
||||||
|
onContextUsage?: (data: ContextUsageEvent) => void;
|
||||||
/** 连接打开 */
|
/** 连接打开 */
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
/** 连接关闭 */
|
/** 连接关闭 */
|
||||||
@ -325,6 +328,9 @@ function dispatchEvent(
|
|||||||
case 'memory_compacted':
|
case 'memory_compacted':
|
||||||
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
|
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
|
||||||
break;
|
break;
|
||||||
|
case 'context_usage':
|
||||||
|
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -313,22 +313,19 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
|
|||||||
* 保存知识图谱到 .iccoder/knowledge.json
|
* 保存知识图谱到 .iccoder/knowledge.json
|
||||||
*/
|
*/
|
||||||
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolder = getWorkspaceFolder();
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolder) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error('请先打开一个工作区');
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
|
||||||
const iccoderDir = path.join(workspacePath, '.iccoder');
|
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
|
||||||
const knowledgePath = path.join(iccoderDir, 'knowledge.json');
|
|
||||||
|
|
||||||
// 确保 .iccoder 目录存在
|
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
||||||
if (!fs.existsSync(iccoderDir)) {
|
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
||||||
fs.mkdirSync(iccoderDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入知识图谱
|
// 写入知识图谱(UTF-8)
|
||||||
fs.writeFileSync(knowledgePath, args.data, 'utf-8');
|
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
|
||||||
|
|
||||||
return `知识图谱已保存: .iccoder/knowledge.json`;
|
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||||
}
|
}
|
||||||
@ -338,21 +335,36 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
|||||||
* 从 .iccoder/knowledge.json 加载知识图谱
|
* 从 .iccoder/knowledge.json 加载知识图谱
|
||||||
*/
|
*/
|
||||||
async function executeKnowledgeLoad(): Promise<string> {
|
async function executeKnowledgeLoad(): Promise<string> {
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolder = getWorkspaceFolder();
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolder) {
|
||||||
throw new Error('请先打开一个工作区');
|
throw new Error('请先打开一个工作区');
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
|
||||||
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json');
|
|
||||||
|
|
||||||
// 如果文件不存在,返回空图谱
|
try {
|
||||||
if (!fs.existsSync(knowledgePath)) {
|
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
|
||||||
return JSON.stringify({ directed: true, nodes: [], links: [] });
|
const content = Buffer.from(bytes).toString('utf-8');
|
||||||
|
return content;
|
||||||
|
} catch (error) {
|
||||||
|
// 文件不存在:返回空图谱
|
||||||
|
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||||
|
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
||||||
|
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
||||||
|
const folders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!folders || folders.length === 0) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(knowledgePath, 'utf-8');
|
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||||
return content;
|
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||||
|
return activeFolder ?? folders[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
|
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
|
||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { submitAnswer } from './apiClient';
|
import { submitAnswer, submitToolConfirm } from './apiClient';
|
||||||
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,6 +32,13 @@ export class UserInteractionManager {
|
|||||||
this.webviewPanel = panel;
|
this.webviewPanel = panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 WebView 面板
|
||||||
|
*/
|
||||||
|
getWebviewPanel(): vscode.WebviewPanel | null {
|
||||||
|
return this.webviewPanel;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 ask_user 事件
|
* 处理 ask_user 事件
|
||||||
* @param event ask_user 事件数据
|
* @param event ask_user 事件数据
|
||||||
@ -60,13 +67,13 @@ export class UserInteractionManager {
|
|||||||
reject
|
reject
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置超时(5分钟)
|
// 设置超时(2小时)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.pendingQuestions.has(askId)) {
|
if (this.pendingQuestions.has(askId)) {
|
||||||
this.pendingQuestions.delete(askId);
|
this.pendingQuestions.delete(askId);
|
||||||
reject(new Error('用户回答超时'));
|
reject(new Error('用户回答超时'));
|
||||||
}
|
}
|
||||||
}, 300000);
|
}, 7200000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,21 +114,46 @@ export class UserInteractionManager {
|
|||||||
taskId: string,
|
taskId: string,
|
||||||
answer: string
|
answer: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const request: AnswerRequest = {
|
// 检查是否是工具确认类型的问题
|
||||||
askId,
|
if (askId.startsWith('tool_confirm_')) {
|
||||||
taskId,
|
// 提取 confirmId
|
||||||
customInput: answer
|
const confirmId = parseInt(askId.replace('tool_confirm_', ''));
|
||||||
};
|
const approved = answer === '确认执行';
|
||||||
|
|
||||||
try {
|
console.log(`[UserInteraction] 提交工具确认: confirmId=${confirmId}, approved=${approved}`);
|
||||||
const response = await submitAnswer(request);
|
|
||||||
if (!response.success) {
|
try {
|
||||||
throw new Error(response.error || '提交回答失败');
|
const response = await submitToolConfirm({
|
||||||
|
confirmId,
|
||||||
|
taskId,
|
||||||
|
approved
|
||||||
|
});
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || '提交工具确认失败');
|
||||||
|
}
|
||||||
|
console.log(`[UserInteraction] 工具确认已提交: confirmId=${confirmId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[UserInteraction] 提交工具确认失败: confirmId=${confirmId}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通问题回答
|
||||||
|
const request: AnswerRequest = {
|
||||||
|
askId,
|
||||||
|
taskId,
|
||||||
|
customInput: answer
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await submitAnswer(request);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || '提交回答失败');
|
||||||
|
}
|
||||||
|
console.log(`[UserInteraction] 回答已提交: askId=${askId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
console.log(`[UserInteraction] 回答已提交: askId=${askId}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,7 @@ export type SSEEventType =
|
|||||||
| 'agent_complete' // 子智能体完成
|
| 'agent_complete' // 子智能体完成
|
||||||
| 'agent_error' // 子智能体错误
|
| 'agent_error' // 子智能体错误
|
||||||
| 'memory_compacted' // 记忆压缩完成
|
| 'memory_compacted' // 记忆压缩完成
|
||||||
|
| 'context_usage' // 上下文使用量
|
||||||
| 'complete' // 对话完成
|
| 'complete' // 对话完成
|
||||||
| 'error' // 错误
|
| 'error' // 错误
|
||||||
| 'warning' // 警告
|
| 'warning' // 警告
|
||||||
@ -181,6 +182,13 @@ export interface AgentErrorEvent {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** context_usage 事件数据 */
|
||||||
|
export interface ContextUsageEvent {
|
||||||
|
currentTokens: number;
|
||||||
|
maxTokens: number;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ============== 工具调用协议 (MCP 格式) ==============
|
// ============== 工具调用协议 (MCP 格式) ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export async function createFile(
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ export async function createFile(
|
|||||||
throw new Error(`文件已存在: ${absolutePath}`);
|
throw new Error(`文件已存在: ${absolutePath}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 如果文件不存在,继续创建
|
// 如果文件不存在,继续创建
|
||||||
if (error.code !== 'FileNotFound') {
|
if (error.code !== "FileNotFound") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +67,9 @@ export async function createOrOverwriteFile(
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +103,9 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法创建相对路径的目录");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您创建目录了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +121,7 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 如果目录不存在,继续创建
|
// 如果目录不存在,继续创建
|
||||||
if (error.code !== 'FileNotFound') {
|
if (error.code !== "FileNotFound") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,7 +167,9 @@ export async function deleteFile(filePath: string): Promise<void> {
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法删除相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您删除文件了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +205,9 @@ export async function updateFile(
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +246,9 @@ export async function appendToFile(
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法追加相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您追加文件内容了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +286,9 @@ export async function replaceFile(
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,14 +305,17 @@ export async function replaceFile(
|
|||||||
|
|
||||||
// 转义特殊字符,将字符串作为字面量处理
|
// 转义特殊字符,将字符串作为字面量处理
|
||||||
const escapeRegExp = (str: string) => {
|
const escapeRegExp = (str: string) => {
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 替换内容 - 如果是字符串,先转义特殊字符
|
// 替换内容 - 如果是字符串,先转义特殊字符
|
||||||
let newContent: string;
|
let newContent: string;
|
||||||
if (typeof searchValue === 'string') {
|
if (typeof searchValue === "string") {
|
||||||
const escapedSearch = escapeRegExp(searchValue);
|
const escapedSearch = escapeRegExp(searchValue);
|
||||||
newContent = fileContent.replace(new RegExp(escapedSearch, "g"), replaceValue);
|
newContent = fileContent.replace(
|
||||||
|
new RegExp(escapedSearch, "g"),
|
||||||
|
replaceValue
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
newContent = fileContent.replace(searchValue, replaceValue);
|
newContent = fileContent.replace(searchValue, replaceValue);
|
||||||
}
|
}
|
||||||
@ -330,7 +347,9 @@ export async function insertAtLine(
|
|||||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,7 +401,9 @@ export async function renameFile(
|
|||||||
absoluteNewPath = path.join(workspaceRoot, newPath);
|
absoluteNewPath = path.join(workspaceRoot, newPath);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("没有打开的工作区,无法重命名相对路径的文件");
|
throw new Error(
|
||||||
|
"请先打开一个文件夹作为工作区,这样我就能为您重命名文件了"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldUri = vscode.Uri.file(absoluteOldPath);
|
const oldUri = vscode.Uri.file(absoluteOldPath);
|
||||||
@ -401,10 +422,13 @@ export async function renameFile(
|
|||||||
throw new Error(`目标文件已存在: ${absoluteNewPath}`);
|
throw new Error(`目标文件已存在: ${absoluteNewPath}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 如果文件不存在,继续重命名
|
// 如果文件不存在,继续重命名
|
||||||
if (error.code !== 'FileNotFound' && !error.message.includes('目标文件已存在')) {
|
if (
|
||||||
|
error.code !== "FileNotFound" &&
|
||||||
|
!error.message.includes("目标文件已存在")
|
||||||
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (error.message.includes('目标文件已存在')) {
|
if (error.message.includes("目标文件已存在")) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { dialogManager, DialogSession } from "../services/dialogService";
|
|||||||
import { userInteractionManager } from "../services/userInteraction";
|
import { userInteractionManager } from "../services/userInteraction";
|
||||||
import { healthCheck } from "../services/apiClient";
|
import { healthCheck } from "../services/apiClient";
|
||||||
|
|
||||||
import type { RunMode } from '../types/api';
|
import type { RunMode } from "../types/api";
|
||||||
|
|
||||||
/** 是否使用后端服务(可通过配置控制) */
|
/** 是否使用后端服务(可通过配置控制) */
|
||||||
let useBackendService = true;
|
let useBackendService = true;
|
||||||
@ -27,12 +27,15 @@ let useBackendService = true;
|
|||||||
/** 当前对话会话 */
|
/** 当前对话会话 */
|
||||||
let currentSession: DialogSession | null = null;
|
let currentSession: DialogSession | null = null;
|
||||||
|
|
||||||
|
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||||
|
let lastTaskId: string | null = null;
|
||||||
|
|
||||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||||
let pendingPlanExecution: {
|
let pendingPlanExecution: {
|
||||||
panel: vscode.WebviewPanel;
|
panel: vscode.WebviewPanel;
|
||||||
planTitle: string;
|
planTitle: string;
|
||||||
extensionPath: string;
|
extensionPath: string;
|
||||||
taskId: string; // 保存 taskId 以便复用
|
taskId: string; // 保存 taskId 以便复用
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,7 +48,7 @@ export function setPendingPlanExecution(
|
|||||||
taskId: string
|
taskId: string
|
||||||
): void {
|
): void {
|
||||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
||||||
console.log('[MessageHandler] 设置待执行计划:', planTitle, 'taskId:', taskId);
|
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,29 +93,28 @@ export async function handleUserMessage(
|
|||||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("后端服务不可用,回退到本地模式:", error);
|
console.error("后端服务不可用:", error);
|
||||||
// 后端不可用时,使用本地模拟回复
|
panel.webview.postMessage({
|
||||||
|
command: "updateStatus",
|
||||||
|
text: "后端服务不可用",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
// 恢复输入状态
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateSegments",
|
||||||
|
segments: [],
|
||||||
|
isComplete: true,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地模拟回复(后端不可用时的 fallback)
|
// 如果没有 extensionPath,显示错误
|
||||||
console.log("使用本地模拟回复");
|
panel.webview.postMessage({
|
||||||
const reply = getMockReply(text);
|
command: "updateStatus",
|
||||||
|
text: "无法处理消息:缺少必要参数",
|
||||||
// 记录AI回复到历史(允许失败)
|
type: "error",
|
||||||
try {
|
});
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
|
||||||
await historyManager.addAiMessage(reply);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("记录AI回复历史失败:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "receiveMessage",
|
|
||||||
text: reply,
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,13 +125,15 @@ async function handleUserMessageWithBackend(
|
|||||||
text: string,
|
text: string,
|
||||||
extensionPath: string,
|
extensionPath: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 创建或复用会话
|
// 创建或复用会话
|
||||||
if (!currentSession || !currentSession.active) {
|
if (!currentSession || !currentSession.active) {
|
||||||
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
|
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
|
||||||
|
// 保存 taskId 用于后续操作(如压缩)
|
||||||
|
lastTaskId = currentSession.getTaskId();
|
||||||
if (reuseTaskId) {
|
if (reuseTaskId) {
|
||||||
console.log('[MessageHandler] 复用 taskId 创建会话:', reuseTaskId);
|
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,117 +147,150 @@ async function handleUserMessageWithBackend(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
currentSession!.sendMessage(text, {
|
currentSession!.sendMessage(
|
||||||
onText: (fullText, isStreaming) => {
|
text,
|
||||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
{
|
||||||
|
onText: (fullText, isStreaming) => {
|
||||||
|
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentUpdate: (segments) => {
|
||||||
|
// 实时发送段落更新,按后端返回顺序展示
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateSegments",
|
||||||
|
segments: segments,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onToolStart: (toolName) => {
|
||||||
|
// 更新状态栏
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateStatus",
|
||||||
|
text: `正在执行 ${toolName}...`,
|
||||||
|
type: "working",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onToolComplete: (toolName, result) => {
|
||||||
|
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||||
|
},
|
||||||
|
|
||||||
|
onToolError: (toolName, error) => {
|
||||||
|
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||||
|
},
|
||||||
|
|
||||||
|
onQuestion: (askId, question, options) => {
|
||||||
|
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateStatus",
|
||||||
|
text: "等待用户回答...",
|
||||||
|
type: "working",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onComplete: async (segments) => {
|
||||||
|
// 隐藏状态栏
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "hideStatus",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 最后一次发送完整的段落
|
||||||
|
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||||||
|
console.log(
|
||||||
|
"[MessageHandler] segments 内容:",
|
||||||
|
JSON.stringify(segments)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await panel.webview.postMessage({
|
||||||
|
command: "updateSegments",
|
||||||
|
segments: segments,
|
||||||
|
isComplete: true,
|
||||||
|
});
|
||||||
|
console.log("[MessageHandler] postMessage 返回值:", result);
|
||||||
|
|
||||||
|
// 保存完整的 segments 到历史记录
|
||||||
|
try {
|
||||||
|
// 将完整的 segments 保存到一条 AI 消息中
|
||||||
|
// 这样加载时可以完整还原对话样式
|
||||||
|
const textContent = segments
|
||||||
|
.filter((s) => s.type === "text" && s.content)
|
||||||
|
.map((s) => s.content)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("保存AI响应历史失败:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||||
|
if (pendingPlanExecution) {
|
||||||
|
const {
|
||||||
|
panel: execPanel,
|
||||||
|
planTitle,
|
||||||
|
extensionPath: execPath,
|
||||||
|
taskId: reuseTaskId,
|
||||||
|
} = pendingPlanExecution;
|
||||||
|
pendingPlanExecution = null;
|
||||||
|
console.log(
|
||||||
|
"[MessageHandler] 自动执行计划:",
|
||||||
|
planTitle,
|
||||||
|
"复用 taskId:",
|
||||||
|
reuseTaskId
|
||||||
|
);
|
||||||
|
|
||||||
|
// 延迟一小段时间确保当前对话完全结束
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||||
|
await handleUserMessageWithBackend(
|
||||||
|
execPanel,
|
||||||
|
`请按照刚才的计划执行:${planTitle}`,
|
||||||
|
execPath,
|
||||||
|
"agent",
|
||||||
|
reuseTaskId // 复用 Plan 模式的 taskId
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[MessageHandler] 自动执行计划失败:", err);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (message) => {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "hideLoading",
|
||||||
|
});
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveMessage",
|
||||||
|
text: `❌ 错误: ${message}`,
|
||||||
|
});
|
||||||
|
// 恢复输入状态
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateSegments",
|
||||||
|
segments: [],
|
||||||
|
isComplete: true,
|
||||||
|
});
|
||||||
|
reject(new Error(message));
|
||||||
|
},
|
||||||
|
|
||||||
|
onNotification: (message) => {
|
||||||
|
vscode.window.showInformationMessage(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
onContextUsage: (data) => {
|
||||||
|
// 发送上下文使用量到 WebView
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "contextUsage",
|
||||||
|
currentTokens: data.currentTokens,
|
||||||
|
maxTokens: data.maxTokens,
|
||||||
|
percentage: data.percentage,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
mode
|
||||||
onSegmentUpdate: (segments) => {
|
);
|
||||||
// 实时发送段落更新,按后端返回顺序展示
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "updateSegments",
|
|
||||||
segments: segments,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onToolStart: (toolName) => {
|
|
||||||
// 更新状态栏
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "updateStatus",
|
|
||||||
text: `正在执行 ${toolName}...`,
|
|
||||||
type: "working",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onToolComplete: (toolName, result) => {
|
|
||||||
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
|
|
||||||
},
|
|
||||||
|
|
||||||
onToolError: (toolName, error) => {
|
|
||||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
|
||||||
},
|
|
||||||
|
|
||||||
onQuestion: (askId, question, options) => {
|
|
||||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "updateStatus",
|
|
||||||
text: "等待用户回答...",
|
|
||||||
type: "working",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onComplete: async (segments) => {
|
|
||||||
// 隐藏状态栏
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "hideStatus",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 最后一次发送完整的段落
|
|
||||||
console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
|
|
||||||
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
|
|
||||||
|
|
||||||
const result = await panel.webview.postMessage({
|
|
||||||
command: "updateSegments",
|
|
||||||
segments: segments,
|
|
||||||
isComplete: true,
|
|
||||||
});
|
|
||||||
console.log('[MessageHandler] postMessage 返回值:', result);
|
|
||||||
|
|
||||||
// 保存完整的 segments 到历史记录
|
|
||||||
try {
|
|
||||||
// 将完整的 segments 保存到一条 AI 消息中
|
|
||||||
// 这样加载时可以完整还原对话样式
|
|
||||||
const textContent = segments
|
|
||||||
.filter(s => s.type === 'text' && s.content)
|
|
||||||
.map(s => s.content)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("保存AI响应历史失败:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
|
||||||
if (pendingPlanExecution) {
|
|
||||||
const { panel: execPanel, planTitle, extensionPath: execPath, taskId: reuseTaskId } = pendingPlanExecution;
|
|
||||||
pendingPlanExecution = null;
|
|
||||||
console.log('[MessageHandler] 自动执行计划:', planTitle, '复用 taskId:', reuseTaskId);
|
|
||||||
|
|
||||||
// 延迟一小段时间确保当前对话完全结束
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
|
||||||
await handleUserMessageWithBackend(
|
|
||||||
execPanel,
|
|
||||||
`请按照刚才的计划执行:${planTitle}`,
|
|
||||||
execPath,
|
|
||||||
'agent',
|
|
||||||
reuseTaskId // 复用 Plan 模式的 taskId
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[MessageHandler] 自动执行计划失败:', err);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: (message) => {
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "hideLoading",
|
|
||||||
});
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "receiveMessage",
|
|
||||||
text: `❌ 错误: ${message}`,
|
|
||||||
});
|
|
||||||
reject(new Error(message));
|
|
||||||
},
|
|
||||||
|
|
||||||
onNotification: (message) => {
|
|
||||||
vscode.window.showInformationMessage(message);
|
|
||||||
},
|
|
||||||
}, mode);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +310,35 @@ export async function handleUserAnswer(
|
|||||||
/**
|
/**
|
||||||
* 中止当前对话
|
* 中止当前对话
|
||||||
*/
|
*/
|
||||||
export function abortCurrentDialog(): void {
|
export async function abortCurrentDialog(): Promise<void> {
|
||||||
|
if (currentSession) {
|
||||||
|
// 保存当前已有的对话内容
|
||||||
|
const segments = currentSession.getSegments();
|
||||||
|
if (segments && segments.length > 0) {
|
||||||
|
try {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const textContent = segments
|
||||||
|
.filter((s) => s.type === "text" && s.content)
|
||||||
|
.map((s) => s.content)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
// 添加中止标记
|
||||||
|
const abortedContent = textContent + "\n\n[对话已被用户中止]";
|
||||||
|
await historyManager.addAiMessage(abortedContent, undefined, segments);
|
||||||
|
console.log("[MessageHandler] 已保存中止前的对话内容");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[MessageHandler] 保存中止对话失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知 WebView 重置分段消息容器
|
||||||
|
const panel = userInteractionManager.getWebviewPanel();
|
||||||
|
if (panel) {
|
||||||
|
panel.webview.postMessage({ command: "resetSegmentedMessage" });
|
||||||
|
console.log("[MessageHandler] 已发送重置分段消息命令");
|
||||||
|
}
|
||||||
|
|
||||||
dialogManager.abortCurrentSession();
|
dialogManager.abortCurrentSession();
|
||||||
currentSession = null;
|
currentSession = null;
|
||||||
}
|
}
|
||||||
@ -282,7 +347,15 @@ export function abortCurrentDialog(): void {
|
|||||||
* 获取当前会话的 taskId
|
* 获取当前会话的 taskId
|
||||||
*/
|
*/
|
||||||
export function getCurrentTaskId(): string | null {
|
export function getCurrentTaskId(): string | null {
|
||||||
return currentSession?.getTaskId() || null;
|
return currentSession?.getTaskId() || lastTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最后的 taskId(加载历史会话时调用)
|
||||||
|
*/
|
||||||
|
export function setLastTaskId(taskId: string): void {
|
||||||
|
lastTaskId = taskId;
|
||||||
|
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,52 +371,52 @@ export async function handlePlanAction(
|
|||||||
planTitle: string,
|
planTitle: string,
|
||||||
extensionPath: string
|
extensionPath: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('[handlePlanAction] action:', action, 'planTitle:', planTitle);
|
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'confirm':
|
case "confirm":
|
||||||
// 确认执行:切换到 Agent 模式并发送执行消息
|
// 确认执行:切换到 Agent 模式并发送执行消息
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: 'switchMode',
|
command: "switchMode",
|
||||||
mode: 'agent'
|
mode: "agent",
|
||||||
});
|
});
|
||||||
// 发送执行消息
|
// 发送执行消息
|
||||||
await handleUserMessage(
|
await handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
`请按照刚才的计划执行:${planTitle}`,
|
`请按照刚才的计划执行:${planTitle}`,
|
||||||
extensionPath,
|
extensionPath,
|
||||||
'agent'
|
"agent"
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'modify':
|
case "modify":
|
||||||
// 修改计划:提示用户输入修改建议
|
// 修改计划:提示用户输入修改建议
|
||||||
const modification = await vscode.window.showInputBox({
|
const modification = await vscode.window.showInputBox({
|
||||||
prompt: '请输入您对计划的修改建议',
|
prompt: "请输入您对计划的修改建议",
|
||||||
placeHolder: '例如:第2步需要先检查文件是否存在...',
|
placeHolder: "例如:第2步需要先检查文件是否存在...",
|
||||||
ignoreFocusOut: true
|
ignoreFocusOut: true,
|
||||||
});
|
});
|
||||||
if (modification) {
|
if (modification) {
|
||||||
await handleUserMessage(
|
await handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
`请根据以下建议修改计划:${modification}`,
|
`请根据以下建议修改计划:${modification}`,
|
||||||
extensionPath,
|
extensionPath,
|
||||||
'plan'
|
"plan"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cancel':
|
case "cancel":
|
||||||
// 取消计划:通知用户
|
// 取消计划:通知用户
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: 'addMessage',
|
command: "addMessage",
|
||||||
text: '计划已取消。',
|
text: "计划已取消。",
|
||||||
sender: 'bot'
|
sender: "bot",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('[handlePlanAction] 未知操作:', action);
|
console.warn("[handlePlanAction] 未知操作:", action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,7 +455,9 @@ function parseFileOperation(text: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
||||||
const renameMatch = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/);
|
const renameMatch = lowerText.match(
|
||||||
|
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
|
||||||
|
);
|
||||||
if (renameMatch) {
|
if (renameMatch) {
|
||||||
const oldPath = renameMatch[1].trim();
|
const oldPath = renameMatch[1].trim();
|
||||||
const newPath = renameMatch[2].trim();
|
const newPath = renameMatch[2].trim();
|
||||||
@ -397,7 +472,9 @@ function parseFileOperation(text: string): {
|
|||||||
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
|
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
|
||||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||||
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
||||||
const replaceMatch1 = lowerText.match(/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
const replaceMatch1 = lowerText.match(
|
||||||
|
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||||
|
);
|
||||||
if (replaceMatch1) {
|
if (replaceMatch1) {
|
||||||
const filePath = replaceMatch1[1].trim();
|
const filePath = replaceMatch1[1].trim();
|
||||||
const searchText = replaceMatch1[2].trim();
|
const searchText = replaceMatch1[2].trim();
|
||||||
@ -411,7 +488,9 @@ function parseFileOperation(text: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||||
const replaceMatch2 = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
const replaceMatch2 = lowerText.match(
|
||||||
|
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||||
|
);
|
||||||
if (replaceMatch2) {
|
if (replaceMatch2) {
|
||||||
const filePath = replaceMatch2[1].trim();
|
const filePath = replaceMatch2[1].trim();
|
||||||
const searchText = replaceMatch2[2].trim();
|
const searchText = replaceMatch2[2].trim();
|
||||||
@ -739,41 +818,6 @@ export async function handleReplaceInFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取模拟回复
|
|
||||||
*/
|
|
||||||
function getMockReply(question: string): string {
|
|
||||||
const replies = [
|
|
||||||
`已收到您的问题:"${question}"
|
|
||||||
|
|
||||||
这是一个演示版本,实际需要连接AI服务。
|
|
||||||
|
|
||||||
示例回复:这是一个计数器模板:
|
|
||||||
\`\`\`verilog
|
|
||||||
module counter (
|
|
||||||
input clk,
|
|
||||||
input rst_n,
|
|
||||||
output reg [3:0] count
|
|
||||||
);
|
|
||||||
always @(posedge clk or negedge rst_n) begin
|
|
||||||
if (!rst_n) count <= 0;
|
|
||||||
else count <= count + 1;
|
|
||||||
end
|
|
||||||
endmodule
|
|
||||||
\`\`\``,
|
|
||||||
|
|
||||||
`感谢提问!关于"${question}",在真实版本中我会:
|
|
||||||
1. 分析您的代码上下文
|
|
||||||
2. 提供优化建议
|
|
||||||
3. 生成完整代码
|
|
||||||
4. 解释设计原理
|
|
||||||
|
|
||||||
当前是演示版,请点击侧边栏按钮快速生成代码。`,
|
|
||||||
];
|
|
||||||
|
|
||||||
return replies[Math.floor(Math.random() * replies.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将代码插入到编辑器
|
* 将代码插入到编辑器
|
||||||
*/
|
*/
|
||||||
@ -866,7 +910,8 @@ async function handleVCDGeneration(
|
|||||||
|
|
||||||
if (!projectCheck.hasTestbench) {
|
if (!projectCheck.hasTestbench) {
|
||||||
errorMsg += "• ❌ 缺少 testbench 文件\n";
|
errorMsg += "• ❌ 缺少 testbench 文件\n";
|
||||||
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
errorMsg +=
|
||||||
|
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||||
} else {
|
} else {
|
||||||
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
|
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
|
||||||
}
|
}
|
||||||
@ -910,9 +955,7 @@ async function handleVCDGeneration(
|
|||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
});
|
});
|
||||||
|
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
|
||||||
`VCD 文件生成成功: ${fileName}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "receiveMessage",
|
command: "receiveMessage",
|
||||||
|
|||||||
@ -24,7 +24,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
{
|
{
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
localResourceRoots: [
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||||
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -39,8 +42,29 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
const iconUri = panel.webview.asWebviewUri(
|
const iconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 获取模型图标URI
|
||||||
|
const autoIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||||
|
);
|
||||||
|
const liteIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||||
|
);
|
||||||
|
const syIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||||
|
);
|
||||||
|
const maxIconUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||||
|
);
|
||||||
|
|
||||||
// 设置HTML内容
|
// 设置HTML内容
|
||||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
panel.webview.html = getWebviewContent(
|
||||||
|
iconUri.toString(),
|
||||||
|
autoIconUri.toString(),
|
||||||
|
liteIconUri.toString(),
|
||||||
|
syIconUri.toString(),
|
||||||
|
maxIconUri.toString()
|
||||||
|
);
|
||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
panel.webview.onDidReceiveMessage(
|
panel.webview.onDidReceiveMessage(
|
||||||
@ -90,7 +114,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
break;
|
break;
|
||||||
// 新增:中止对话
|
// 新增:中止对话
|
||||||
case "abortDialog":
|
case "abortDialog":
|
||||||
abortCurrentDialog();
|
void abortCurrentDialog();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -136,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(
|
||||||
|
|||||||
@ -2,310 +2,206 @@
|
|||||||
* 智能体卡片组件
|
* 智能体卡片组件
|
||||||
*
|
*
|
||||||
* 功能说明:
|
* 功能说明:
|
||||||
* - 显示子智能体的执行过程
|
* - 提供智能体执行状态的可视化展示
|
||||||
* - 支持展开/收起步骤详情
|
* - 显示智能体名称、状态和执行步骤
|
||||||
* - 显示执行状态和统计信息
|
* - 支持实时更新步骤信息
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import { agentIconSvg } from "../constants/toolIcons";
|
||||||
AgentStartEvent,
|
|
||||||
AgentProgressEvent,
|
|
||||||
AgentCompleteEvent,
|
|
||||||
AgentErrorEvent
|
|
||||||
} from '../types/api';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取智能体卡片样式
|
* 获取智能体卡片的样式
|
||||||
*/
|
*/
|
||||||
export function getAgentCardStyles(): string {
|
export function getAgentCardStyles(): string {
|
||||||
return `
|
return `
|
||||||
|
/* 智能体卡片样式 */
|
||||||
|
.segment-agent {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
.agent-card {
|
.agent-card {
|
||||||
background: var(--vscode-editor-background);
|
|
||||||
border: 1px solid var(--vscode-input-border);
|
border: 1px solid var(--vscode-input-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 10px 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
}
|
}
|
||||||
.agent-card-header {
|
.agent-header {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: var(--vscode-sideBar-background);
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.agent-card-header:hover {
|
|
||||||
background: var(--vscode-list-hoverBackground);
|
|
||||||
}
|
|
||||||
.agent-card-title {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-weight: 500;
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
border-bottom: 1px solid var(--vscode-input-border);
|
||||||
}
|
}
|
||||||
.agent-card-icon {
|
.agent-icon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.agent-card-status {
|
.agent-name {
|
||||||
font-size: 12px;
|
font-weight: 500;
|
||||||
padding: 2px 6px;
|
flex: 1;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
.agent-card-status.running {
|
.agent-status {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.agent-status.running {
|
||||||
background: var(--vscode-inputValidation-infoBackground);
|
background: var(--vscode-inputValidation-infoBackground);
|
||||||
color: var(--vscode-inputValidation-infoForeground);
|
color: var(--vscode-inputValidation-infoForeground);
|
||||||
}
|
}
|
||||||
.agent-card-status.completed {
|
.agent-status.completed {
|
||||||
background: var(--vscode-testing-iconPassed);
|
background: #28a745;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.agent-card-status.error {
|
.agent-status.error {
|
||||||
background: var(--vscode-testing-iconFailed);
|
background: #dc3545;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.agent-card-toggle {
|
.agent-body {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.agent-steps-container {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
}
|
|
||||||
.agent-card-body {
|
|
||||||
padding: 12px;
|
|
||||||
border-top: 1px solid var(--vscode-input-border);
|
|
||||||
}
|
|
||||||
.agent-card-body.collapsed {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.agent-card-instruction {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px dashed var(--vscode-input-border);
|
|
||||||
}
|
|
||||||
.agent-steps {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
.agent-step {
|
.agent-step {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 6px 0;
|
padding: 4px 8px;
|
||||||
border-left: 2px solid var(--vscode-input-border);
|
border-radius: 4px;
|
||||||
padding-left: 12px;
|
margin-bottom: 4px;
|
||||||
margin-left: 6px;
|
background: var(--vscode-list-hoverBackground);
|
||||||
}
|
}
|
||||||
.agent-step:last-child {
|
.agent-step:last-child {
|
||||||
border-left-color: transparent;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.agent-step-icon {
|
.step-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.agent-step-content {
|
.step-name {
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.agent-step-name {
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
}
|
}
|
||||||
.agent-step-result {
|
.step-result {
|
||||||
font-size: 12px;
|
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
margin-top: 2px;
|
font-size: 11px;
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.agent-card-summary {
|
.agent-step-placeholder {
|
||||||
font-size: 13px;
|
color: var(--vscode-descriptionForeground);
|
||||||
padding: 8px 12px;
|
font-style: italic;
|
||||||
background: var(--vscode-sideBar-background);
|
padding: 8px;
|
||||||
border-top: 1px solid var(--vscode-input-border);
|
text-align: center;
|
||||||
}
|
}
|
||||||
.agent-card-error {
|
/* 低调显示的工具调用样式 */
|
||||||
color: var(--vscode-errorForeground);
|
.agent-step.low-profile {
|
||||||
padding: 8px 12px;
|
opacity: 0.5;
|
||||||
background: var(--vscode-inputValidation-errorBackground);
|
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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染智能体卡片(启动状态)
|
* 获取智能体卡片的脚本
|
||||||
*/
|
|
||||||
export function renderAgentCardStart(event: AgentStartEvent): string {
|
|
||||||
return `
|
|
||||||
<div class="agent-card" id="agent-${event.agentId}">
|
|
||||||
<div class="agent-card-header" onclick="toggleAgentCard('${event.agentId}')">
|
|
||||||
<div class="agent-card-title">
|
|
||||||
<span class="agent-card-icon">🤖</span>
|
|
||||||
<span>${event.agentName}</span>
|
|
||||||
<span class="agent-card-status running">执行中</span>
|
|
||||||
</div>
|
|
||||||
<span class="agent-card-toggle">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="agent-card-body" id="agent-body-${event.agentId}">
|
|
||||||
<div class="agent-card-instruction">指令:${escapeHtml(event.instruction)}</div>
|
|
||||||
<div class="agent-steps" id="agent-steps-${event.agentId}">
|
|
||||||
<!-- 步骤将动态添加 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渲染步骤项(进行中)
|
|
||||||
*/
|
|
||||||
export function renderAgentStepRunning(event: AgentProgressEvent): string {
|
|
||||||
const inputStr = event.toolInput ? JSON.stringify(event.toolInput) : '';
|
|
||||||
const shortInput = inputStr.length > 50 ? inputStr.substring(0, 50) + '...' : inputStr;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="agent-step" id="agent-step-${event.agentId}-${event.step}">
|
|
||||||
<span class="agent-step-icon">🔄</span>
|
|
||||||
<div class="agent-step-content">
|
|
||||||
<div class="agent-step-name">${event.toolName}</div>
|
|
||||||
<div class="agent-step-result">${escapeHtml(shortInput)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新步骤项(完成)
|
|
||||||
*/
|
|
||||||
export function getStepCompleteUpdate(event: AgentProgressEvent): { icon: string; result: string } {
|
|
||||||
const result = event.toolResult || '';
|
|
||||||
const shortResult = result.length > 80 ? result.substring(0, 80) + '...' : result;
|
|
||||||
return {
|
|
||||||
icon: event.status === 'completed' ? '✅' : '❌',
|
|
||||||
result: shortResult
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取智能体卡片脚本
|
|
||||||
*/
|
*/
|
||||||
export function getAgentCardScript(): string {
|
export function getAgentCardScript(): string {
|
||||||
return `
|
return `
|
||||||
// 智能体状态存储
|
// 工具名称中文映射
|
||||||
const agentStates = {};
|
function getAgentToolDisplayName(toolName) {
|
||||||
|
const toolNameMap = {
|
||||||
// 切换智能体卡片展开/收起
|
'file_read': '文件读取',
|
||||||
function toggleAgentCard(agentId) {
|
'file_write': '文件写入',
|
||||||
const body = document.getElementById('agent-body-' + agentId);
|
'file_delete': '文件删除',
|
||||||
const header = body?.previousElementSibling;
|
'file_list': '检索文件',
|
||||||
const toggle = header?.querySelector('.agent-card-toggle');
|
'syntax_check': '语法检查',
|
||||||
|
'simulation': '仿真',
|
||||||
if (body && toggle) {
|
'waveform_summary': '波形分析',
|
||||||
body.classList.toggle('collapsed');
|
'knowledge_save': '保存知识库',
|
||||||
toggle.textContent = body.classList.contains('collapsed') ? '▶' : '▼';
|
'knowledge_load': '加载知识库',
|
||||||
}
|
'queryKnowledgeSummary': '查询知识摘要',
|
||||||
}
|
'queryRules': '查询规则',
|
||||||
|
'setModule': '设置模块',
|
||||||
// 处理智能体启动事件
|
'addSignal': '正在分析信号定义',
|
||||||
function handleAgentStart(event) {
|
'addSignalExample': '正在处理信号示例',
|
||||||
agentStates[event.agentId] = {
|
'validateKnowledgeGraph': '验证知识图谱',
|
||||||
status: 'running',
|
'querySignals': '查询信号',
|
||||||
steps: []
|
'addPlan': '添加计划',
|
||||||
|
'addEdge': '添加边',
|
||||||
|
'showPlan': '显示计划',
|
||||||
|
'spawnExplorer': '代码探索'
|
||||||
};
|
};
|
||||||
|
return toolNameMap[toolName] || toolName;
|
||||||
// 在当前消息中添加智能体卡片
|
|
||||||
const currentMessage = document.querySelector('.bot-message:last-child .message-content');
|
|
||||||
if (currentMessage) {
|
|
||||||
currentMessage.insertAdjacentHTML('beforeend', renderAgentCardStart(event));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理智能体进度事件
|
/**
|
||||||
function handleAgentProgress(event) {
|
* 渲染智能体卡片
|
||||||
const stepsContainer = document.getElementById('agent-steps-' + event.agentId);
|
* @param {Object} segment - 智能体段落数据
|
||||||
if (!stepsContainer) return;
|
* @param {HTMLElement} segmentDiv - 段落容器元素
|
||||||
|
*/
|
||||||
|
function renderAgentCard(segment, segmentDiv) {
|
||||||
|
segmentDiv.className += ' segment-agent';
|
||||||
|
|
||||||
if (event.status === 'running') {
|
const statusText = segment.agentStatus === 'completed' ? '完成'
|
||||||
// 添加新步骤
|
: segment.agentStatus === 'error' ? '错误' : '执行中';
|
||||||
stepsContainer.insertAdjacentHTML('beforeend', renderAgentStepRunning(event));
|
const statusClass = segment.agentStatus || 'running';
|
||||||
} else if (event.status === 'completed') {
|
|
||||||
// 更新步骤状态
|
const stepsHtml = (segment.agentSteps || []).map(step => {
|
||||||
const stepEl = document.getElementById('agent-step-' + event.agentId + '-' + event.step);
|
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
||||||
if (stepEl) {
|
const displayName = getAgentToolDisplayName(step.toolName);
|
||||||
const iconEl = stepEl.querySelector('.agent-step-icon');
|
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
|
||||||
const resultEl = stepEl.querySelector('.agent-step-result');
|
// 为技术性工具调用添加低调样式(用户看不懂的)
|
||||||
if (iconEl) iconEl.textContent = '✅';
|
const lowProfileTools = [
|
||||||
if (resultEl) {
|
'knowledge_save', 'knowledge_load',
|
||||||
const result = event.toolResult || '';
|
'queryKnowledgeSummary', 'queryRules', 'querySignals',
|
||||||
resultEl.textContent = result.length > 80 ? result.substring(0, 80) + '...' : result;
|
'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('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
<div class="agent-card">
|
||||||
|
<div class="agent-header">
|
||||||
|
<span class="agent-icon">${agentIconSvg}</span>
|
||||||
|
<span class="agent-name">\${segment.agentName || '智能体'}</span>
|
||||||
|
<span class="agent-status \${statusClass}">\${statusText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-body">
|
||||||
|
<div class="agent-steps-container">
|
||||||
|
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
// 自动滚动到最新步骤
|
||||||
|
setTimeout(() => {
|
||||||
|
const container = segmentDiv.querySelector('.agent-steps-container');
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}, 0);
|
||||||
}
|
|
||||||
|
|
||||||
// 处理智能体完成事件
|
|
||||||
function handleAgentComplete(event) {
|
|
||||||
const card = document.getElementById('agent-' + event.agentId);
|
|
||||||
if (!card) return;
|
|
||||||
|
|
||||||
// 更新状态
|
|
||||||
const statusEl = card.querySelector('.agent-card-status');
|
|
||||||
if (statusEl) {
|
|
||||||
statusEl.className = 'agent-card-status completed';
|
|
||||||
statusEl.textContent = '完成';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加摘要
|
|
||||||
const body = card.querySelector('.agent-card-body');
|
|
||||||
if (body) {
|
|
||||||
body.insertAdjacentHTML('beforeend',
|
|
||||||
'<div class="agent-card-summary">' + escapeHtml(event.summary) + '</div>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动收起
|
|
||||||
body?.classList.add('collapsed');
|
|
||||||
const toggle = card.querySelector('.agent-card-toggle');
|
|
||||||
if (toggle) toggle.textContent = '▶';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理智能体错误事件
|
|
||||||
function handleAgentError(event) {
|
|
||||||
const card = document.getElementById('agent-' + event.agentId);
|
|
||||||
if (!card) return;
|
|
||||||
|
|
||||||
// 更新状态
|
|
||||||
const statusEl = card.querySelector('.agent-card-status');
|
|
||||||
if (statusEl) {
|
|
||||||
statusEl.className = 'agent-card-status error';
|
|
||||||
statusEl.textContent = '错误';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加错误信息
|
|
||||||
const body = card.querySelector('.agent-card-body');
|
|
||||||
if (body) {
|
|
||||||
body.insertAdjacentHTML('beforeend',
|
|
||||||
'<div class="agent-card-error">错误:' + escapeHtml(event.error) + '</div>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTML 转义
|
|
||||||
function escapeHtml(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HTML 转义(服务端使用)
|
|
||||||
*/
|
|
||||||
function escapeHtml(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
return text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* 模式选择器组件
|
* 模式选择器组件
|
||||||
* 提供 Plan/Ask/Agent/Auto 四种模式的选择功能
|
* 提供 Plan/Ask/Agent 四种模式的选择功能
|
||||||
*
|
*
|
||||||
* 模式说明:
|
* 模式说明:
|
||||||
* - Plan: 只读模式,只能查询分析,不能写文件
|
* - Plan: 只读模式,只能查询分析,不能写文件
|
||||||
* - Ask: 逐个确认,每个写操作需用户确认
|
* - Ask: 逐个确认,每个写操作需用户确认
|
||||||
* - Agent: 智能体自主,自动执行大部分操作
|
* - Agent: 智能体自主,自动执行大部分操作
|
||||||
* - Auto: 完全自动,所有操作自动执行
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
plannerIconSvg,
|
||||||
|
askIconSvg,
|
||||||
|
agentIconSvg,
|
||||||
|
} from "../constants/toolIcons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取模式选择器的 HTML 内容
|
* 获取模式选择器的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -24,20 +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>
|
||||||
</div>
|
<span class="mode-option-label">Agent</span>
|
||||||
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">
|
</div>
|
||||||
<span class="mode-option-label">Auto</span>
|
<span class="mode-option-desc">用于快速生成工程、调试修改现有代码</span>
|
||||||
<span class="mode-option-desc">完全自动</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -88,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;
|
||||||
@ -103,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);
|
||||||
@ -117,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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -162,10 +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模式'
|
||||||
'auto': '完全自动 - 所有操作自动执行'
|
|
||||||
};
|
};
|
||||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
225
src/views/contextDisplay.ts
Normal 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();
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -2,60 +2,63 @@ import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
|||||||
import {
|
import {
|
||||||
getModelSelectorContent,
|
getModelSelectorContent,
|
||||||
getModelSelectorStyles,
|
getModelSelectorStyles,
|
||||||
getModelSelectorScript
|
getModelSelectorScript,
|
||||||
} from "./modelSelector";
|
} from "./modelSelector";
|
||||||
import {
|
import {
|
||||||
getModeSelectorContent,
|
getModeSelectorContent,
|
||||||
getModeSelectorStyles,
|
getModeSelectorStyles,
|
||||||
getModeSelectorScript
|
getModeSelectorScript,
|
||||||
} from "./agentModeSelector";
|
} from "./agentModeSelector";
|
||||||
import {
|
import {
|
||||||
getContextButtonContent,
|
getContextButtonContent,
|
||||||
getContextButtonStyles,
|
getContextButtonStyles,
|
||||||
getContextButtonScript
|
getContextButtonScript,
|
||||||
} from "./contextButton";
|
} from "./contextButton";
|
||||||
|
import {
|
||||||
|
getContextDisplayContent,
|
||||||
|
getContextDisplayStyles,
|
||||||
|
getContextDisplayScript,
|
||||||
|
} from "./contextDisplay";
|
||||||
import {
|
import {
|
||||||
getContextCompressContent,
|
getContextCompressContent,
|
||||||
getContextCompressStyles,
|
getContextCompressStyles,
|
||||||
getContextCompressScript
|
getContextCompressScript,
|
||||||
} from "./contextCompress";
|
} from "./contextCompress";
|
||||||
import {
|
|
||||||
getPlanToggleContent,
|
|
||||||
getPlanToggleStyles,
|
|
||||||
getPlanToggleScript
|
|
||||||
} from "./planToggle";
|
|
||||||
import {
|
import {
|
||||||
getOptimizeButtonContent,
|
getOptimizeButtonContent,
|
||||||
getOptimizeButtonStyles,
|
getOptimizeButtonStyles,
|
||||||
getOptimizeButtonScript
|
getOptimizeButtonScript,
|
||||||
} from "./optimizeButton";
|
} from "./optimizeButton";
|
||||||
import {
|
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||||
sendIconSvg,
|
|
||||||
stopIconSvg
|
|
||||||
} from "../constants/toolIcons";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取输入区域的 HTML 内容
|
* 获取输入区域的 HTML 内容
|
||||||
*/
|
*/
|
||||||
export function getInputAreaContent(): string {
|
export function getInputAreaContent(
|
||||||
|
autoIcon: string = '',
|
||||||
|
liteIcon: string = '',
|
||||||
|
syIcon: string = '',
|
||||||
|
maxIcon: 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()}
|
||||||
${getPlanToggleContent()}
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 上下文显示区域 -->
|
||||||
|
${getContextDisplayContent()}
|
||||||
<textarea
|
<textarea
|
||||||
id="messageInput"
|
id="messageInput"
|
||||||
placeholder="输入您的问题..."
|
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
||||||
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
|
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="input-bottom-row">
|
<div class="input-bottom-row">
|
||||||
<div class="mode-selector">
|
<div class="mode-selector">
|
||||||
${getModeSelectorContent()}
|
${getModeSelectorContent()}
|
||||||
${getModelSelectorContent()}
|
${getModelSelectorContent(autoIcon, liteIcon, syIcon, maxIcon)}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-actions">
|
<div class="input-actions">
|
||||||
${getContextCompressContent()}
|
${getContextCompressContent()}
|
||||||
@ -80,13 +83,30 @@ export function getInputAreaStyles(): string {
|
|||||||
${getModeSelectorStyles()}
|
${getModeSelectorStyles()}
|
||||||
${getModelSelectorStyles()}
|
${getModelSelectorStyles()}
|
||||||
${getContextButtonStyles()}
|
${getContextButtonStyles()}
|
||||||
|
${getContextDisplayStyles()}
|
||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getPlanToggleStyles()}
|
|
||||||
${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;
|
||||||
@ -207,6 +227,11 @@ export function getInputAreaStyles(): string {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
textarea:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: rgba(128, 128, 128, 0.1);
|
||||||
|
}
|
||||||
/* 简洁的滚动条样式 */
|
/* 简洁的滚动条样式 */
|
||||||
textarea::-webkit-scrollbar {
|
textarea::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -264,12 +289,33 @@ export function getInputAreaScript(): string {
|
|||||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||||
${getModelSelectorScript()}
|
${getModelSelectorScript()}
|
||||||
${getContextButtonScript()}
|
${getContextButtonScript()}
|
||||||
|
${getContextDisplayScript()}
|
||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getPlanToggleScript()}
|
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
|
let hasMessages = false; // 是否已有消息
|
||||||
|
|
||||||
|
// 工作区检测状态
|
||||||
|
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||||||
|
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() {
|
||||||
@ -283,11 +329,16 @@ export function getInputAreaScript(): string {
|
|||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
messageInput.addEventListener('input', autoResizeTextarea);
|
messageInput.addEventListener('input', autoResizeTextarea);
|
||||||
|
|
||||||
|
// 监听点击事件,检测工作区状态
|
||||||
|
messageInput.addEventListener('focus', () => {
|
||||||
|
if (!hasCheckedWorkspace) {
|
||||||
|
hasCheckedWorkspace = true;
|
||||||
|
vscode.postMessage({ command: 'checkWorkspace' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化时调整一次高度
|
// 初始化时调整一次高度
|
||||||
autoResizeTextarea();
|
autoResizeTextarea();
|
||||||
|
|
||||||
// 聚焦到输入框
|
|
||||||
messageInput.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换发送按钮状态
|
// 切换发送按钮状态
|
||||||
@ -302,11 +353,17 @@ export function getInputAreaScript(): string {
|
|||||||
sendIconContainer.style.display = 'none';
|
sendIconContainer.style.display = 'none';
|
||||||
stopIconContainer.style.display = 'block';
|
stopIconContainer.style.display = 'block';
|
||||||
isConversationActive = true;
|
isConversationActive = true;
|
||||||
|
// 禁用输入框
|
||||||
|
messageInput.disabled = true;
|
||||||
|
messageInput.placeholder = '正在处理中,请稍候...';
|
||||||
} else {
|
} else {
|
||||||
sendButton.classList.remove('sending');
|
sendButton.classList.remove('sending');
|
||||||
sendIconContainer.style.display = 'block';
|
sendIconContainer.style.display = 'block';
|
||||||
stopIconContainer.style.display = 'none';
|
stopIconContainer.style.display = 'none';
|
||||||
isConversationActive = false;
|
isConversationActive = false;
|
||||||
|
// 启用输入框
|
||||||
|
messageInput.disabled = false;
|
||||||
|
messageInput.placeholder = '输入您的问题,按 Enter 发送,Shift + Enter 换行...';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,16 +383,43 @@ export function getInputAreaScript(): string {
|
|||||||
const text = messageInput.value.trim();
|
const text = messageInput.value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
|
// 如果正在对话中,阻止发送新消息
|
||||||
|
if (isConversationActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查工作区状态
|
||||||
|
if (!hasWorkspace) {
|
||||||
|
// 如果没有工作区,阻止发送并清空输入框
|
||||||
|
messageInput.value = '';
|
||||||
|
autoResizeTextarea();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
||||||
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();
|
||||||
@ -343,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);
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,13 +13,23 @@
|
|||||||
import {
|
import {
|
||||||
collapseIconSvg,
|
collapseIconSvg,
|
||||||
fileWriteIconSvg,
|
fileWriteIconSvg,
|
||||||
|
fileReadIconSvg,
|
||||||
|
fileDeleteIconSvg,
|
||||||
syntaxCheckIconSvg,
|
syntaxCheckIconSvg,
|
||||||
SearchCode,
|
SearchCode,
|
||||||
|
agentIconSvg,
|
||||||
|
saveKnowledgeIconSvg,
|
||||||
|
simulationIconSvg,
|
||||||
|
waveformIconSvg,
|
||||||
|
knowledgeLoadIconSvg,
|
||||||
|
stateTransitionIconSvg,
|
||||||
} from "../constants/toolIcons";
|
} from "../constants/toolIcons";
|
||||||
import {
|
import {
|
||||||
getWaveformPreviewContent,
|
getWaveformPreviewContent,
|
||||||
getWaveformPreviewScript,
|
getWaveformPreviewScript,
|
||||||
} from "./waveformPreviewContent";
|
} from "./waveformPreviewContent";
|
||||||
|
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||||
|
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取消息区域的 HTML 内容
|
* 获取消息区域的 HTML 内容
|
||||||
@ -363,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;
|
||||||
@ -400,7 +416,7 @@ export function getMessageAreaStyles(): string {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||||
transform: rotate(0deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
@ -416,6 +432,28 @@ export function getMessageAreaStyles(): string {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.tool-file-read-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-file-read-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tool-file-delete-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-file-delete-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.tool-syntax-check-icon {
|
.tool-syntax-check-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@ -438,6 +476,61 @@ export function getMessageAreaStyles(): string {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.tool-save-knowledge-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-save-knowledge-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tool-simulation-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-simulation-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tool-waveform-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-waveform-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tool-knowledge-load-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-knowledge-load-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tool-state-transition-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.tool-state-transition-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.tool-segment-content {
|
.tool-segment-content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 0.3s ease;
|
transition: max-height 0.3s ease;
|
||||||
@ -445,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;
|
||||||
@ -528,170 +638,9 @@ export function getMessageAreaStyles(): string {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;}
|
font-size: 12px;}
|
||||||
|
|
||||||
/* 智能体卡片样式 */
|
${getAgentCardStyles()}
|
||||||
.segment-agent {
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
.agent-card {
|
|
||||||
border: 1px solid var(--vscode-input-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--vscode-editor-background);
|
|
||||||
}
|
|
||||||
.agent-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: var(--vscode-sideBar-background);
|
|
||||||
border-bottom: 1px solid var(--vscode-input-border);
|
|
||||||
}
|
|
||||||
.agent-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.agent-name {
|
|
||||||
font-weight: 500;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.agent-status {
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.agent-status.running {
|
|
||||||
background: var(--vscode-inputValidation-infoBackground);
|
|
||||||
color: var(--vscode-inputValidation-infoForeground);
|
|
||||||
}
|
|
||||||
.agent-status.completed {
|
|
||||||
background: #28a745;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.agent-status.error {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.agent-body {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
.agent-steps-container {
|
|
||||||
max-height: 150px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.agent-step {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
background: var(--vscode-list-hoverBackground);
|
|
||||||
}
|
|
||||||
.agent-step:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.step-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.step-name {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--vscode-foreground);
|
|
||||||
}
|
|
||||||
.step-result {
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
font-size: 11px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.agent-step-placeholder {
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
font-style: italic;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 计划卡片样式 */
|
${getPlanCardStyles()}
|
||||||
.segment-plan {
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
.plan-card {
|
|
||||||
border: 1px solid var(--vscode-input-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--vscode-editor-background);
|
|
||||||
}
|
|
||||||
.plan-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: var(--vscode-sideBar-background);
|
|
||||||
border-bottom: 1px solid var(--vscode-input-border);
|
|
||||||
}
|
|
||||||
.plan-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.plan-title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.plan-body {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
.plan-summary {
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.plan-steps {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.plan-step {
|
|
||||||
padding: 6px 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
background: var(--vscode-list-hoverBackground);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.plan-step:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.step-num {
|
|
||||||
color: var(--vscode-textLink-foreground);
|
|
||||||
font-weight: 500;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
.plan-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
border-top: 1px solid var(--vscode-input-border);
|
|
||||||
background: var(--vscode-sideBar-background);
|
|
||||||
}
|
|
||||||
.plan-btn {
|
|
||||||
padding: 6px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.plan-btn-confirm {
|
|
||||||
background: var(--vscode-button-background);
|
|
||||||
color: var(--vscode-button-foreground);
|
|
||||||
}
|
|
||||||
.plan-btn-confirm:hover {
|
|
||||||
background: var(--vscode-button-hoverBackground);
|
|
||||||
}
|
|
||||||
.plan-btn-modify {
|
|
||||||
background: var(--vscode-input-background);
|
|
||||||
color: var(--vscode-foreground);
|
|
||||||
border: 1px solid var(--vscode-input-border);
|
|
||||||
}
|
|
||||||
.plan-btn-cancel {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
}
|
|
||||||
|
|
||||||
${getWaveformPreviewContent()}
|
${getWaveformPreviewContent()}
|
||||||
`;
|
`;
|
||||||
@ -705,18 +654,75 @@ export function getMessageAreaScript(): string {
|
|||||||
// 工具图标定义
|
// 工具图标定义
|
||||||
const collapseIconSvg = \`${collapseIconSvg}\`;
|
const collapseIconSvg = \`${collapseIconSvg}\`;
|
||||||
const fileWriteIconSvg = \`${fileWriteIconSvg}\`;
|
const fileWriteIconSvg = \`${fileWriteIconSvg}\`;
|
||||||
|
const fileReadIconSvg = \`${fileReadIconSvg}\`;
|
||||||
|
const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`;
|
||||||
const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`;
|
const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`;
|
||||||
const searchCodeIconSvg = \`${SearchCode}\`;
|
const searchCodeIconSvg = \`${SearchCode}\`;
|
||||||
|
const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`;
|
||||||
|
const simulationIconSvg = \`${simulationIconSvg}\`;
|
||||||
|
const waveformIconSvg = \`${waveformIconSvg}\`;
|
||||||
|
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||||
|
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||||
|
|
||||||
|
${getAgentCardScript()}
|
||||||
|
|
||||||
|
${getPlanCardScript()}
|
||||||
|
|
||||||
|
// 获取工具图标
|
||||||
|
function getToolIcon(toolName) {
|
||||||
|
const iconMap = {
|
||||||
|
'file_read': fileReadIconSvg,
|
||||||
|
'file_write': fileWriteIconSvg,
|
||||||
|
'file_delete': fileDeleteIconSvg,
|
||||||
|
'file_list': searchCodeIconSvg,
|
||||||
|
'syntax_check': syntaxCheckIconSvg,
|
||||||
|
'simulation': simulationIconSvg,
|
||||||
|
'waveform_summary': waveformIconSvg,
|
||||||
|
'knowledge_save': saveKnowledgeIconSvg,
|
||||||
|
'knowledge_load': knowledgeLoadIconSvg,
|
||||||
|
'queryKnowledgeSummary': knowledgeLoadIconSvg,
|
||||||
|
'queryRules': knowledgeLoadIconSvg,
|
||||||
|
'setModule': fileWriteIconSvg,
|
||||||
|
'addSignal': fileWriteIconSvg,
|
||||||
|
'addSignalExample': fileWriteIconSvg,
|
||||||
|
'validateKnowledgeGraph': syntaxCheckIconSvg,
|
||||||
|
'querySignals': searchCodeIconSvg,
|
||||||
|
'addPlan': fileWriteIconSvg,
|
||||||
|
'addEdge': fileWriteIconSvg,
|
||||||
|
'showPlan': searchCodeIconSvg,
|
||||||
|
'addRule': fileWriteIconSvg,
|
||||||
|
'updateNode': fileWriteIconSvg,
|
||||||
|
'addStateTransition': stateTransitionIconSvg
|
||||||
|
};
|
||||||
|
return iconMap[toolName] || '';
|
||||||
|
}
|
||||||
|
|
||||||
// 工具名称映射
|
// 工具名称映射
|
||||||
function getToolDisplayName(toolName) {
|
function getToolDisplayName(toolName) {
|
||||||
const toolNameMap = {
|
const toolNameMap = {
|
||||||
'file_read': '已完成文件读取',
|
'file_read': '已完成文件读取',
|
||||||
'file_write': '已完成文件写入',
|
'file_write': '已完成文件写入',
|
||||||
|
'file_delete': '已完成文件删除',
|
||||||
'file_list': '已检索代码文件',
|
'file_list': '已检索代码文件',
|
||||||
'syntax_check': '已完成语法检查',
|
'syntax_check': '已完成语法检查',
|
||||||
'simulation': '已完成仿真',
|
'simulation': '已完成仿真',
|
||||||
'waveform_summary': '已完成波形分析'
|
'waveform_summary': '已完成波形分析',
|
||||||
|
'knowledge_save': '已保存知识库',
|
||||||
|
'knowledge_load': '已加载知识库',
|
||||||
|
'queryKnowledgeSummary': '已查询知识摘要',
|
||||||
|
'queryRules': '已查询规则',
|
||||||
|
'setModule': '已设置模块',
|
||||||
|
'addSignal': '信号分析完成',
|
||||||
|
'addSignalExample': '信号示例处理完成',
|
||||||
|
'validateKnowledgeGraph': '已验证知识图谱',
|
||||||
|
'querySignals': '已查询信号',
|
||||||
|
'addPlan': '已添加计划',
|
||||||
|
'addEdge': '已添加边',
|
||||||
|
'showPlan': '已显示计划',
|
||||||
|
'addRule': '已添加规则',
|
||||||
|
'updateNode': '已更新节点',
|
||||||
|
'addStateTransition': '已添加状态转换',
|
||||||
|
'spawnExplorer': '代码探索'
|
||||||
};
|
};
|
||||||
return toolNameMap[toolName] || toolName;
|
return toolNameMap[toolName] || toolName;
|
||||||
}
|
}
|
||||||
@ -907,6 +913,9 @@ export function getMessageAreaScript(): string {
|
|||||||
// 存储已回答问题的状态
|
// 存储已回答问题的状态
|
||||||
const answeredQuestions = new Map(); // askId -> answer
|
const answeredQuestions = new Map(); // askId -> answer
|
||||||
|
|
||||||
|
// 存储工具展开/折叠状态
|
||||||
|
const toolCollapseStates = new Map(); // index -> isCollapsed
|
||||||
|
|
||||||
// 实时更新分段消息(按后端返回顺序)
|
// 实时更新分段消息(按后端返回顺序)
|
||||||
function updateSegmentsRealtime(segments, isComplete) {
|
function updateSegmentsRealtime(segments, isComplete) {
|
||||||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||||||
@ -938,10 +947,42 @@ export function getMessageAreaScript(): string {
|
|||||||
messagesEl.appendChild(currentSegmentedMessage);
|
messagesEl.appendChild(currentSegmentedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存当前所有工具的展开/折叠状态
|
||||||
|
if (currentSegmentedMessage) {
|
||||||
|
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||||||
|
toolHeaders.forEach((header, idx) => {
|
||||||
|
const isCollapsed = header.classList.contains('collapsed');
|
||||||
|
toolCollapseStates.set(idx, isCollapsed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 清空容器并重新渲染所有段落
|
// 清空容器并重新渲染所有段落
|
||||||
currentSegmentedMessage.innerHTML = '';
|
currentSegmentedMessage.innerHTML = '';
|
||||||
|
|
||||||
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toolIndex = 0; // 用于跟踪工具段落的索引
|
||||||
|
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,22 +990,44 @@ export function getMessageAreaScript(): string {
|
|||||||
segmentDiv.className += ' segment-text';
|
segmentDiv.className += ' segment-text';
|
||||||
segmentDiv.innerHTML = formatText(segment.content);
|
segmentDiv.innerHTML = formatText(segment.content);
|
||||||
} else if (segment.type === 'tool') {
|
} else if (segment.type === 'tool') {
|
||||||
|
// 过滤掉不需要显示的工具
|
||||||
|
if (segment.toolName === 'spawnExplorer') {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 恢复之前保存的展开/折叠状态
|
||||||
|
const savedState = toolCollapseStates.get(toolIndex);
|
||||||
|
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
|
||||||
|
const currentToolIndex = toolIndex;
|
||||||
|
toolIndex++; // 递增工具索引
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||||
\${shouldCollapse ? collapseIconSvg : ''}
|
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
|
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
|
|
||||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
|
|
||||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</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\${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>\` : ''}
|
||||||
\`;
|
\`;
|
||||||
|
|
||||||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||||||
@ -990,27 +1053,24 @@ export function getMessageAreaScript(): string {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||||
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
|
|
||||||
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
|
|
||||||
|
|
||||||
if (header && content) {
|
if (header && content) {
|
||||||
header.addEventListener('click', function() {
|
header.addEventListener('click', function() {
|
||||||
const isCollapsed = header.classList.contains('collapsed');
|
const isCollapsed = header.classList.contains('collapsed');
|
||||||
|
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
// 展开
|
// 展开
|
||||||
header.classList.remove('collapsed');
|
header.classList.remove('collapsed');
|
||||||
content.classList.remove('collapsed');
|
content.classList.remove('collapsed');
|
||||||
content.style.maxHeight = content.scrollHeight + 'px';
|
content.style.maxHeight = content.scrollHeight + 'px';
|
||||||
if (iconCollapsed) iconCollapsed.style.display = 'none';
|
toolCollapseStates.set(toolIdx, false);
|
||||||
if (iconExpanded) iconExpanded.style.display = 'block';
|
|
||||||
} else {
|
} else {
|
||||||
// 折叠
|
// 折叠
|
||||||
header.classList.add('collapsed');
|
header.classList.add('collapsed');
|
||||||
content.classList.add('collapsed');
|
content.classList.add('collapsed');
|
||||||
content.style.maxHeight = '0';
|
content.style.maxHeight = '0';
|
||||||
if (iconCollapsed) iconCollapsed.style.display = 'block';
|
toolCollapseStates.set(toolIdx, true);
|
||||||
if (iconExpanded) iconExpanded.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1083,126 +1143,10 @@ export function getMessageAreaScript(): string {
|
|||||||
}
|
}
|
||||||
} else if (segment.type === 'agent') {
|
} else if (segment.type === 'agent') {
|
||||||
// 智能体卡片渲染
|
// 智能体卡片渲染
|
||||||
segmentDiv.className += ' segment-agent';
|
renderAgentCard(segment, segmentDiv);
|
||||||
const statusText = segment.agentStatus === 'completed' ? '完成'
|
|
||||||
: segment.agentStatus === 'error' ? '错误' : '执行中';
|
|
||||||
const statusClass = segment.agentStatus || 'running';
|
|
||||||
|
|
||||||
const stepsHtml = (segment.agentSteps || []).map(step => {
|
|
||||||
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
|
||||||
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">\${step.toolName}</span><span class="step-result">\${result}</span></div>\`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
|
||||||
<div class="agent-card">
|
|
||||||
<div class="agent-header">
|
|
||||||
<span class="agent-icon">🤖</span>
|
|
||||||
<span class="agent-name">\${segment.agentName || '智能体'}</span>
|
|
||||||
<span class="agent-status \${statusClass}">\${statusText}</span>
|
|
||||||
</div>
|
|
||||||
<div class="agent-body">
|
|
||||||
<div class="agent-steps-container">
|
|
||||||
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
\`;
|
|
||||||
|
|
||||||
// 自动滚动到最新步骤
|
|
||||||
setTimeout(() => {
|
|
||||||
const container = segmentDiv.querySelector('.agent-steps-container');
|
|
||||||
if (container) {
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
} else if (segment.type === 'plan') {
|
} else if (segment.type === 'plan') {
|
||||||
// 计划卡片渲染(类似 askUser)
|
// 计划卡片渲染(使用独立组件)
|
||||||
segmentDiv.className += ' segment-plan';
|
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||||||
|
|
||||||
// 检查是否已回答
|
|
||||||
const isAnswered = answeredQuestions.has(segment.askId);
|
|
||||||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
|
||||||
|
|
||||||
if (isAnswered) {
|
|
||||||
segmentDiv.classList.add('answered');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
|
||||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
// 选项按钮
|
|
||||||
const options = ['确认执行', '修改计划', '取消'];
|
|
||||||
const optionsHtml = options.map(opt => {
|
|
||||||
const isSelected = isAnswered && opt === selectedAnswer;
|
|
||||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
|
||||||
<div class="plan-card">
|
|
||||||
<div class="plan-header">
|
|
||||||
<span class="plan-icon">📋</span>
|
|
||||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="plan-body">
|
|
||||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
|
||||||
<div class="plan-steps">\${stepsHtml}</div>
|
|
||||||
</div>
|
|
||||||
<div class="plan-actions">
|
|
||||||
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
|
|
||||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
|
||||||
<input type="text" class="custom-input" placeholder="输入修改建议..." />
|
|
||||||
<button class="custom-submit">提交</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
\`;
|
|
||||||
|
|
||||||
// 只在未回答时添加事件监听
|
|
||||||
if (!isAnswered) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
|
||||||
optionButtons.forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const option = this.getAttribute('data-option');
|
|
||||||
// 发送答案到后端
|
|
||||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
|
||||||
// 同时发送 planAction 用于模式切换
|
|
||||||
const actionMap = {
|
|
||||||
'确认执行': 'confirm',
|
|
||||||
'修改计划': 'modify',
|
|
||||||
'取消': 'cancel'
|
|
||||||
};
|
|
||||||
vscode.postMessage({
|
|
||||||
command: 'planAction',
|
|
||||||
action: actionMap[option] || option,
|
|
||||||
planTitle: segment.planTitle
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
|
||||||
const customInput = segmentDiv.querySelector('.custom-input');
|
|
||||||
if (submitBtn && customInput) {
|
|
||||||
submitBtn.addEventListener('click', function() {
|
|
||||||
const customValue = customInput.value.trim();
|
|
||||||
if (customValue) {
|
|
||||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
customInput.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const customValue = customInput.value.trim();
|
|
||||||
if (customValue) {
|
|
||||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSegmentedMessage.appendChild(segmentDiv);
|
currentSegmentedMessage.appendChild(segmentDiv);
|
||||||
@ -1213,9 +1157,11 @@ export function getMessageAreaScript(): string {
|
|||||||
console.log('[WebView] 对话完成,添加操作按钮');
|
console.log('[WebView] 对话完成,添加操作按钮');
|
||||||
const actionsDiv = document.createElement('div');
|
const actionsDiv = document.createElement('div');
|
||||||
actionsDiv.className = 'message-actions';
|
actionsDiv.className = 'message-actions';
|
||||||
|
|
||||||
|
// 复制按钮
|
||||||
const copyBtn = document.createElement('button');
|
const copyBtn = document.createElement('button');
|
||||||
copyBtn.className = 'action-btn';
|
copyBtn.className = 'action-btn';
|
||||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||||
copyBtn.onclick = () => {
|
copyBtn.onclick = () => {
|
||||||
const textContent = segments
|
const textContent = segments
|
||||||
.filter(s => s.type === 'text' && s.content)
|
.filter(s => s.type === 'text' && s.content)
|
||||||
@ -1223,7 +1169,22 @@ export function getMessageAreaScript(): string {
|
|||||||
.join('\\n');
|
.join('\\n');
|
||||||
copyMessage(textContent, copyBtn);
|
copyMessage(textContent, copyBtn);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 点赞按钮
|
||||||
|
const likeBtn = document.createElement('button');
|
||||||
|
likeBtn.className = 'action-btn';
|
||||||
|
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||||
|
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||||
|
|
||||||
|
// 点踩按钮
|
||||||
|
const dislikeBtn = document.createElement('button');
|
||||||
|
dislikeBtn.className = 'action-btn';
|
||||||
|
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||||
|
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||||
|
|
||||||
actionsDiv.appendChild(copyBtn);
|
actionsDiv.appendChild(copyBtn);
|
||||||
|
actionsDiv.appendChild(likeBtn);
|
||||||
|
actionsDiv.appendChild(dislikeBtn);
|
||||||
currentSegmentedMessage.appendChild(actionsDiv);
|
currentSegmentedMessage.appendChild(actionsDiv);
|
||||||
|
|
||||||
// 重置当前分段消息容器
|
// 重置当前分段消息容器
|
||||||
@ -1261,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;
|
||||||
|
|
||||||
@ -1269,19 +1252,35 @@ export function getMessageAreaScript(): string {
|
|||||||
segmentDiv.className += ' segment-text';
|
segmentDiv.className += ' segment-text';
|
||||||
segmentDiv.innerHTML = formatText(segment.content);
|
segmentDiv.innerHTML = formatText(segment.content);
|
||||||
} else if (segment.type === 'tool') {
|
} else if (segment.type === 'tool') {
|
||||||
|
// 过滤掉不需要显示的工具
|
||||||
|
if (segment.toolName === 'spawnExplorer') {
|
||||||
|
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;
|
||||||
|
|
||||||
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 ? collapseIconSvg : ''}
|
\${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 && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
|
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
|
|
||||||
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
|
|
||||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</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>\` : ''}
|
||||||
@ -1310,27 +1309,24 @@ export function getMessageAreaScript(): string {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||||
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
|
|
||||||
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
|
|
||||||
|
|
||||||
if (header && content) {
|
if (header && content) {
|
||||||
header.addEventListener('click', function() {
|
header.addEventListener('click', function() {
|
||||||
const isCollapsed = header.classList.contains('collapsed');
|
const isCollapsed = header.classList.contains('collapsed');
|
||||||
|
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
// 展开
|
// 展开
|
||||||
header.classList.remove('collapsed');
|
header.classList.remove('collapsed');
|
||||||
content.classList.remove('collapsed');
|
content.classList.remove('collapsed');
|
||||||
content.style.maxHeight = content.scrollHeight + 'px';
|
content.style.maxHeight = content.scrollHeight + 'px';
|
||||||
if (iconCollapsed) iconCollapsed.style.display = 'none';
|
toolCollapseStates.set(toolIdx, false);
|
||||||
if (iconExpanded) iconExpanded.style.display = 'block';
|
|
||||||
} else {
|
} else {
|
||||||
// 折叠
|
// 折叠
|
||||||
header.classList.add('collapsed');
|
header.classList.add('collapsed');
|
||||||
content.classList.add('collapsed');
|
content.classList.add('collapsed');
|
||||||
content.style.maxHeight = '0';
|
content.style.maxHeight = '0';
|
||||||
if (iconCollapsed) iconCollapsed.style.display = 'block';
|
toolCollapseStates.set(toolIdx, true);
|
||||||
if (iconExpanded) iconExpanded.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1347,72 +1343,10 @@ export function getMessageAreaScript(): string {
|
|||||||
\`;
|
\`;
|
||||||
} else if (segment.type === 'agent') {
|
} else if (segment.type === 'agent') {
|
||||||
// 智能体卡片渲染
|
// 智能体卡片渲染
|
||||||
segmentDiv.className += ' segment-agent';
|
renderAgentCard(segment, segmentDiv);
|
||||||
const statusText = segment.agentStatus === 'completed' ? '完成'
|
|
||||||
: segment.agentStatus === 'error' ? '错误' : '执行中';
|
|
||||||
const statusClass = segment.agentStatus || 'running';
|
|
||||||
|
|
||||||
const stepsHtml = (segment.agentSteps || []).map(step => {
|
|
||||||
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
|
||||||
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">\${step.toolName}</span><span class="step-result">\${result}</span></div>\`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
|
||||||
<div class="agent-card">
|
|
||||||
<div class="agent-header">
|
|
||||||
<span class="agent-icon">🤖</span>
|
|
||||||
<span class="agent-name">\${segment.agentName || '智能体'}</span>
|
|
||||||
<span class="agent-status \${statusClass}">\${statusText}</span>
|
|
||||||
</div>
|
|
||||||
<div class="agent-body">
|
|
||||||
<div class="agent-steps-container">
|
|
||||||
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
\`;
|
|
||||||
} else if (segment.type === 'plan') {
|
} else if (segment.type === 'plan') {
|
||||||
// 计划卡片渲染
|
// 计划卡片渲染(使用独立组件)
|
||||||
segmentDiv.className += ' segment-plan';
|
renderPlanCardStatic(segment, segmentDiv);
|
||||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
|
||||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
|
||||||
<div class="plan-card">
|
|
||||||
<div class="plan-header">
|
|
||||||
<span class="plan-icon">📋</span>
|
|
||||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="plan-body">
|
|
||||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
|
||||||
<div class="plan-steps">\${stepsHtml}</div>
|
|
||||||
</div>
|
|
||||||
<div class="plan-actions">
|
|
||||||
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
|
||||||
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
|
||||||
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
\`;
|
|
||||||
|
|
||||||
// 绑定按钮事件
|
|
||||||
setTimeout(() => {
|
|
||||||
const planCard = segmentDiv.querySelector('.plan-card');
|
|
||||||
if (planCard) {
|
|
||||||
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
const action = e.currentTarget?.dataset?.action;
|
|
||||||
vscode.postMessage({
|
|
||||||
command: 'planAction',
|
|
||||||
action: action,
|
|
||||||
planTitle: segment.planTitle
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(segmentDiv);
|
container.appendChild(segmentDiv);
|
||||||
@ -1421,9 +1355,11 @@ export function getMessageAreaScript(): string {
|
|||||||
// 添加操作按钮
|
// 添加操作按钮
|
||||||
const actionsDiv = document.createElement('div');
|
const actionsDiv = document.createElement('div');
|
||||||
actionsDiv.className = 'message-actions';
|
actionsDiv.className = 'message-actions';
|
||||||
|
|
||||||
|
// 复制按钮
|
||||||
const copyBtn = document.createElement('button');
|
const copyBtn = document.createElement('button');
|
||||||
copyBtn.className = 'action-btn';
|
copyBtn.className = 'action-btn';
|
||||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||||
copyBtn.onclick = () => {
|
copyBtn.onclick = () => {
|
||||||
const textContent = segments
|
const textContent = segments
|
||||||
.filter(s => s.type === 'text' && s.content)
|
.filter(s => s.type === 'text' && s.content)
|
||||||
@ -1431,7 +1367,22 @@ export function getMessageAreaScript(): string {
|
|||||||
.join('\\n');
|
.join('\\n');
|
||||||
copyMessage(textContent, copyBtn);
|
copyMessage(textContent, copyBtn);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 点赞按钮
|
||||||
|
const likeBtn = document.createElement('button');
|
||||||
|
likeBtn.className = 'action-btn';
|
||||||
|
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||||
|
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||||
|
|
||||||
|
// 点踩按钮
|
||||||
|
const dislikeBtn = document.createElement('button');
|
||||||
|
dislikeBtn.className = 'action-btn';
|
||||||
|
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||||
|
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||||
|
|
||||||
actionsDiv.appendChild(copyBtn);
|
actionsDiv.appendChild(copyBtn);
|
||||||
|
actionsDiv.appendChild(likeBtn);
|
||||||
|
actionsDiv.appendChild(dislikeBtn);
|
||||||
container.appendChild(actionsDiv);
|
container.appendChild(actionsDiv);
|
||||||
|
|
||||||
messagesEl.appendChild(container);
|
messagesEl.appendChild(container);
|
||||||
|
|||||||
@ -5,7 +5,12 @@
|
|||||||
/**
|
/**
|
||||||
* 获取模型选择器的 HTML 内容
|
* 获取模型选择器的 HTML 内容
|
||||||
*/
|
*/
|
||||||
export function getModelSelectorContent(): string {
|
export function getModelSelectorContent(
|
||||||
|
autoIcon: string = "",
|
||||||
|
liteIcon: string = "",
|
||||||
|
syIcon: string = "",
|
||||||
|
maxIcon: string = ""
|
||||||
|
): string {
|
||||||
return `
|
return `
|
||||||
<!-- 模型选择 -->
|
<!-- 模型选择 -->
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
@ -17,13 +22,51 @@ export function getModelSelectorContent(): string {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-dropdown" id="modelDropdown">
|
<div class="select-dropdown" id="modelDropdown">
|
||||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">Lite</div>
|
<div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
|
||||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">Auto</div>
|
${
|
||||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">Syntaxic</div>
|
autoIcon
|
||||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">Max</div>
|
? `<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 class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
|
||||||
|
${
|
||||||
|
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 class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||||
|
${
|
||||||
|
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 class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
|
||||||
|
${
|
||||||
|
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>
|
||||||
<!-- 模型选择器的 tooltip 容器 -->
|
|
||||||
<div id="modelTooltip" class="model-tooltip"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="tooltiptext">选择模型</span>
|
<span class="tooltiptext">选择模型</span>
|
||||||
</div>
|
</div>
|
||||||
@ -87,11 +130,13 @@ export function getModelSelectorStyles(): string {
|
|||||||
/* 模型选择器的选项样式 */
|
/* 模型选择器的选项样式 */
|
||||||
#modelDropdown .select-option {
|
#modelDropdown .select-option {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 6px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
white-space: nowrap;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
#modelDropdown .select-option:hover {
|
#modelDropdown .select-option:hover {
|
||||||
background: rgba(128, 128, 128, 0.3);
|
background: rgba(128, 128, 128, 0.3);
|
||||||
@ -100,49 +145,28 @@ export function getModelSelectorStyles(): string {
|
|||||||
background: rgba(128, 128, 128, 0.5);
|
background: rgba(128, 128, 128, 0.5);
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
}
|
}
|
||||||
/* 模型选择器的 tooltip 样式 */
|
.model-icon {
|
||||||
.model-tooltip {
|
width: 16px;
|
||||||
position: fixed;
|
height: 16px;
|
||||||
background: #1e1e1e;
|
flex-shrink: 0;
|
||||||
color: #ffffff;
|
object-fit: contain;
|
||||||
padding: 8px 12px;
|
}
|
||||||
border-radius: 6px;
|
.option-content {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.option-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
font-weight: 500;
|
||||||
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 {
|
.option-desc {
|
||||||
opacity: 1;
|
font-size: 11px;
|
||||||
visibility: visible;
|
color: var(--vscode-descriptionForeground);
|
||||||
}
|
white-space: nowrap;
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -205,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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
293
src/views/planCard.ts
Normal file
293
src/views/planCard.ts
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
/**
|
||||||
|
* 计划卡片组件
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 显示执行计划的卡片界面
|
||||||
|
* - 包含计划标题、摘要和步骤列表
|
||||||
|
* - 提供确认执行、修改计划、取消等操作按钮
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { plannerIconSvg } from "../constants/toolIcons";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计划卡片的样式
|
||||||
|
*/
|
||||||
|
export function getPlanCardStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 计划卡片样式 */
|
||||||
|
.segment-plan {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.plan-card {
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
.plan-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
border-bottom: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.plan-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.plan-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.plan-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.plan-summary {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.plan-steps {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.plan-step {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.plan-step:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.step-checkbox {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
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 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-top: 1px solid var(--vscode-input-border);
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
}
|
||||||
|
.plan-actions .question-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.plan-btn {
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.plan-btn-confirm {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
.plan-btn-confirm:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
.plan-btn-modify {
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.plan-btn-cancel {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.plan-actions .custom-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.plan-actions .custom-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-input-foreground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.plan-actions .custom-submit {
|
||||||
|
padding: 8px 18px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.plan-actions .custom-submit:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计划卡片的脚本
|
||||||
|
*/
|
||||||
|
export function getPlanCardScript(): string {
|
||||||
|
return `
|
||||||
|
// 渲染计划卡片(在 updateSegmentsRealtime 中使用)
|
||||||
|
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
|
||||||
|
segmentDiv.className += ' segment-plan';
|
||||||
|
|
||||||
|
// 检查是否已回答
|
||||||
|
const isAnswered = answeredQuestions.has(segment.askId);
|
||||||
|
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||||||
|
|
||||||
|
if (isAnswered) {
|
||||||
|
segmentDiv.classList.add('answered');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||||
|
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// 选项按钮
|
||||||
|
const options = ['确认执行', '修改计划', '取消'];
|
||||||
|
const optionsHtml = options.map(opt => {
|
||||||
|
const isSelected = isAnswered && opt === selectedAnswer;
|
||||||
|
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
<div class="plan-card">
|
||||||
|
<div class="plan-header">
|
||||||
|
<span class="plan-icon">${plannerIconSvg}</span>
|
||||||
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="plan-body">
|
||||||
|
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||||
|
<div class="plan-steps">\${stepsHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div class="plan-actions">
|
||||||
|
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
|
||||||
|
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||||
|
<input type="text" class="custom-input" placeholder="输入修改建议..." />
|
||||||
|
<button class="custom-submit">提交</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
// 只在未回答时添加事件监听
|
||||||
|
if (!isAnswered) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||||
|
optionButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const option = this.getAttribute('data-option');
|
||||||
|
// 发送答案到后端
|
||||||
|
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||||
|
// 同时发送 planAction 用于模式切换
|
||||||
|
const actionMap = {
|
||||||
|
'确认执行': 'confirm',
|
||||||
|
'修改计划': 'modify',
|
||||||
|
'取消': 'cancel'
|
||||||
|
};
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'planAction',
|
||||||
|
action: actionMap[option] || option,
|
||||||
|
planTitle: segment.planTitle
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||||
|
const customInput = segmentDiv.querySelector('.custom-input');
|
||||||
|
if (submitBtn && customInput) {
|
||||||
|
submitBtn.addEventListener('click', function() {
|
||||||
|
const customValue = customInput.value.trim();
|
||||||
|
if (customValue) {
|
||||||
|
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const customValue = customInput.value.trim();
|
||||||
|
if (customValue) {
|
||||||
|
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染计划卡片(在 renderSegments 中使用)
|
||||||
|
function renderPlanCardStatic(segment, segmentDiv) {
|
||||||
|
segmentDiv.className += ' segment-plan';
|
||||||
|
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||||
|
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
<div class="plan-card">
|
||||||
|
<div class="plan-header">
|
||||||
|
<span class="plan-icon">📋</span>
|
||||||
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="plan-body">
|
||||||
|
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||||
|
<div class="plan-steps">\${stepsHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div class="plan-actions">
|
||||||
|
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
||||||
|
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
||||||
|
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
// 绑定按钮事件
|
||||||
|
setTimeout(() => {
|
||||||
|
const planCard = segmentDiv.querySelector('.plan-card');
|
||||||
|
if (planCard) {
|
||||||
|
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const action = e.currentTarget?.dataset?.action;
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'planAction',
|
||||||
|
action: action,
|
||||||
|
planTitle: segment.planTitle
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
411
src/views/progressBar.ts
Normal file
411
src/views/progressBar.ts
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
178
src/views/thinkingProcess.ts
Normal file
178
src/views/thinkingProcess.ts
Normal 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);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -263,61 +263,63 @@ export function getWaveformPreviewScript(): string {
|
|||||||
const timeRange = maxTime - minTime || 1;
|
const timeRange = maxTime - minTime || 1;
|
||||||
|
|
||||||
// 绘制波形
|
// 绘制波形
|
||||||
let pathData = '';
|
if (signal.width === 1) {
|
||||||
let lastX = leftMargin;
|
// 单比特信号 - 绘制数字波形
|
||||||
let lastValue = signal.values[0].value;
|
let pathData = '';
|
||||||
|
const yHigh = y;
|
||||||
|
const yLow = y + signalHeight;
|
||||||
|
|
||||||
signal.values.forEach((point, i) => {
|
signal.values.forEach((point, i) => {
|
||||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||||
const value = point.value;
|
const currentY = (point.value === '1') ? yHigh : yLow;
|
||||||
|
|
||||||
if (signal.width === 1) {
|
|
||||||
// 单比特信号 - 绘制数字波形
|
|
||||||
const yHigh = y;
|
|
||||||
const yLow = y + signalHeight;
|
|
||||||
const currentY = (value === '1') ? yHigh : yLow;
|
|
||||||
|
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
pathData = \`M \${x} \${currentY}\`;
|
pathData = \`M \${x} \${currentY}\`;
|
||||||
} else {
|
} else {
|
||||||
// 绘制垂直跳变
|
const prevValue = signal.values[i - 1].value;
|
||||||
const prevY = (lastValue === '1') ? yHigh : yLow;
|
const prevY = (prevValue === '1') ? yHigh : yLow;
|
||||||
if (prevY !== currentY) {
|
if (prevY !== currentY) {
|
||||||
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
|
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
|
||||||
} else {
|
} else {
|
||||||
pathData += \` L \${x} \${currentY}\`;
|
pathData += \` L \${x} \${currentY}\`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
lastValue = value;
|
// 延伸到右边界
|
||||||
lastX = x;
|
const lastValue = signal.values[signal.values.length - 1].value;
|
||||||
} else {
|
const lastY = (lastValue === '1') ? yHigh : yLow;
|
||||||
// 多比特信号 - 绘制总线波形(梯形)
|
|
||||||
const yTop = y + 5;
|
|
||||||
const yBottom = y + signalHeight - 5;
|
|
||||||
const transitionWidth = 5;
|
|
||||||
|
|
||||||
if (i === 0) {
|
|
||||||
pathData = \`M \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
|
||||||
} else {
|
|
||||||
// 绘制梯形过渡
|
|
||||||
pathData += \` L \${x - transitionWidth} \${yTop} L \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastX = x;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 延伸到右边界
|
|
||||||
if (signal.width === 1) {
|
|
||||||
const lastY = (lastValue === '1') ? y : (y + signalHeight);
|
|
||||||
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
|
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
|
||||||
} else {
|
|
||||||
const yMid = y + signalHeight / 2;
|
|
||||||
pathData += \` L \${leftMargin + waveformWidth} \${yMid}\`;
|
|
||||||
}
|
|
||||||
|
|
||||||
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||||
|
} else {
|
||||||
|
// 多比特信号 - 绘制总线波形(上下双线)
|
||||||
|
const yTop = y + 5;
|
||||||
|
const yBottom = y + signalHeight - 5;
|
||||||
|
const transitionWidth = 4;
|
||||||
|
|
||||||
|
let topPath = \`M \${leftMargin} \${yTop}\`;
|
||||||
|
let bottomPath = \`M \${leftMargin} \${yBottom}\`;
|
||||||
|
|
||||||
|
signal.values.forEach((point, i) => {
|
||||||
|
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||||
|
|
||||||
|
// 上线和下线都延伸到变化点
|
||||||
|
topPath += \` L \${x} \${yTop}\`;
|
||||||
|
bottomPath += \` L \${x} \${yBottom}\`;
|
||||||
|
|
||||||
|
// 绘制梯形过渡
|
||||||
|
topPath += \` L \${x + transitionWidth} \${yBottom} L \${x + transitionWidth} \${yTop}\`;
|
||||||
|
bottomPath += \` L \${x + transitionWidth} \${yTop} L \${x + transitionWidth} \${yBottom}\`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 延伸到右边界
|
||||||
|
topPath += \` L \${leftMargin + waveformWidth} \${yTop}\`;
|
||||||
|
bottomPath += \` L \${leftMargin + waveformWidth} \${yBottom}\`;
|
||||||
|
|
||||||
|
svgContent += \`<path d="\${topPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||||
|
svgContent += \`<path d="\${bottomPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绘制时间轴
|
// 绘制时间轴
|
||||||
|
|||||||
@ -17,14 +17,27 @@ import {
|
|||||||
getMessageAreaStyles,
|
getMessageAreaStyles,
|
||||||
getMessageAreaScript,
|
getMessageAreaScript,
|
||||||
} from "./messageArea";
|
} from "./messageArea";
|
||||||
|
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||||
import {
|
import {
|
||||||
getAgentCardStyles,
|
getProgressBarContent,
|
||||||
getAgentCardScript,
|
getProgressBarStyles,
|
||||||
} from "./agentCard";
|
getProgressBarScript,
|
||||||
|
} from "./progressBar";
|
||||||
|
import { getCurrentEnv } from "../config/settings";
|
||||||
/**
|
/**
|
||||||
* 获取 WebView 面板的 HTML 内容
|
* 获取 WebView 面板的 HTML 内容
|
||||||
*/
|
*/
|
||||||
export function getWebviewContent(iconUri?: string): string {
|
export function getWebviewContent(
|
||||||
|
iconUri?: string,
|
||||||
|
autoIconUri?: string,
|
||||||
|
liteIconUri?: string,
|
||||||
|
syIconUri?: string,
|
||||||
|
maxIconUri?: 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>
|
||||||
@ -72,6 +85,7 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
${getAgentCardStyles()}
|
${getAgentCardStyles()}
|
||||||
${getWaveformPreviewContent()}
|
${getWaveformPreviewContent()}
|
||||||
${getConversationHistoryBarStyles()}
|
${getConversationHistoryBarStyles()}
|
||||||
|
${getProgressBarStyles()}
|
||||||
${getInputAreaStyles()}
|
${getInputAreaStyles()}
|
||||||
|
|
||||||
.file-editor-section {
|
.file-editor-section {
|
||||||
@ -377,6 +391,7 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
</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;" />
|
||||||
@ -394,14 +409,18 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
<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()}
|
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -440,10 +459,9 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
}
|
}
|
||||||
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] || '切换模式';
|
||||||
}
|
}
|
||||||
@ -499,8 +517,6 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messageInput.focus();
|
|
||||||
|
|
||||||
// 监听来自插件的消息
|
// 监听来自插件的消息
|
||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
@ -567,11 +583,32 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'resetSegmentedMessage':
|
||||||
|
// 重置分段消息容器(停止对话时调用)
|
||||||
|
console.log('[WebView] 重置分段消息容器');
|
||||||
|
currentSegmentedMessage = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'contextUsage':
|
||||||
|
// 更新上下文使用量显示
|
||||||
|
if (typeof updateContextDisplay === 'function') {
|
||||||
|
updateContextDisplay(message.currentTokens, message.maxTokens);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'hideLoading':
|
case 'hideLoading':
|
||||||
// 隐藏加载指示器
|
// 隐藏加载指示器
|
||||||
hideLoadingIndicator();
|
hideLoadingIndicator();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'workspaceStatus':
|
||||||
|
// 更新工作区状态
|
||||||
|
if (typeof hasWorkspace !== 'undefined') {
|
||||||
|
hasWorkspace = message.hasWorkspace;
|
||||||
|
console.log('[WebView] 工作区状态:', hasWorkspace);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'vcdInfo':
|
case 'vcdInfo':
|
||||||
// 渲染迷你波形预览信息
|
// 渲染迷你波形预览信息
|
||||||
try {
|
try {
|
||||||
@ -628,6 +665,10 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
if (messagesContainer) {
|
if (messagesContainer) {
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
// 重置输入框布局到居中
|
||||||
|
if (typeof window.resetInputAreaLayout === 'function') {
|
||||||
|
window.resetInputAreaLayout();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'addUserMessage':
|
case 'addUserMessage':
|
||||||
@ -635,6 +676,10 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
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':
|
||||||
@ -642,6 +687,10 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
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':
|
||||||
@ -674,6 +723,7 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
${getAgentCardScript()}
|
${getAgentCardScript()}
|
||||||
${getWaveformPreviewScript()}
|
${getWaveformPreviewScript()}
|
||||||
${getConversationHistoryBarScript()}
|
${getConversationHistoryBarScript()}
|
||||||
|
${getProgressBarScript()}
|
||||||
${getInputAreaScript()}
|
${getInputAreaScript()}
|
||||||
</script></body>
|
</script></body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user