Skip to content

Commit 121801b

Browse files
committed
fix: resolve file paths and searches across every workspace folder
- Accept all VS Code workspace folders in the tool execution context and check containment against their union, so sibling folders of a multi-root workspace stop failing with 'path must stay inside the VS Code workspace'. - Route paths prefixed with a sibling folder name ('micro-server/src/...') into that folder instead of creating a same-named fake subdirectory under the primary folder. - Default search_files/search_code sweep every workspace folder in a multi-root workspace and prefix results with the folder name. - Extract workspaceRoots helpers out of the gateway to stay within the file line budget.
1 parent 738b404 commit 121801b

14 files changed

Lines changed: 432 additions & 58 deletions
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# 会话交接报告 2026-10-24
2+
3+
> 状态:进行中,协议漂移检测已落盘但未验证
4+
> 原因:会话上下文接近上限,被迫中断
5+
6+
## 1. 已完成并验证
7+
8+
### 1.1 提示词:批量派工规则 6
9+
10+
- 文件:`gateway-vscode/prompts/prompt_zh.md` / `prompt_en.md`
11+
- 提交:`b07bad1`
12+
- 验证:真实 DeepSeek 会话中 3 个 read_file 在 17ms 间隔内同轮发出,批次显示 3/3 结果已回填
13+
- 局限:只验证了无依赖的简单读取,未验证跨轮混合依赖场景
14+
15+
### 1.2 DeepSeek maxInlineChars
16+
17+
- 文件:`gateway-vscode/src/platforms.ts`
18+
- 验证:32.35KB 初始化上下文走 .txt 附件,DeepSeek 前端认账
19+
20+
### 1.3 read_file 附件路由(代码层)
21+
22+
- 提交:`b7ea22f`
23+
- 文件:`tools/attachmentSupport.ts`(新建)、`attachFileTool.ts`、`readFileTool.ts`、`readFileAttachmentRouting.test.ts`
24+
- 验证:391 项单测全绿
25+
- 未完成:模型实测偏好 attach_file 而非 read_file,路由未被自然触发;配套的 attach_file 描述去反向暗示未做
26+
27+
### 1.4 依赖漏洞 C→B 全量推进
28+
29+
- 提交:`4d08be4`、`8ec4d5e`、`ad0c628`、`b8175a8`、`a914d53`
30+
- 结果:audit:shipped 从 33 条降到 0 条
31+
- 已推送远程
32+
- 注意:Dependabot 页面可能滞后,需重扫后确认
33+
34+
### 1.5 仓库描述与来源措辞
35+
36+
- 提交:`ebb4dba`
37+
- 结果:去掉 fork 表述,isFork 为 false
38+
39+
## 2. 已落盘但未验证(本会话中断点)
40+
41+
### 2.1 协议漂移检测
42+
43+
文件:
44+
45+
- `bridge-browser/src/modules/nativeToolSyntax.ts`(新建,纯检测函数)
46+
- `bridge-browser/test/native_tool_syntax.test.ts`(新建,6 条用例)
47+
- `bridge-browser/src/content/main.ts`(+33 行:导入、主循环插入、reportNativeSyntaxDrift 函数)
48+
49+
设计要点:
50+
51+
- 检测 DeepSeek DSML 与 Anthropic antml 两类原生语法
52+
- 只在生成结束后(Stop 按钮不可见)且本轮无有效 JSON 调用时提示
53+
- 用 WeakMap 按消息元素去重,避免流式重扫刷屏
54+
- 走 showUserAttentionNotification 通道
55+
56+
必须执行的验证:
57+
58+
1. `pnpm --filter bridge-browser run build` 编译
59+
2. `pnpm --filter bridge-browser run test` 跑单测
60+
3. 确认 main.ts 的 i18n 导入改动未破坏其他调用
61+
62+
风险:未编译,可能存在类型错误或导入问题。
63+
64+
## 3. 未开始
65+
66+
- train_zh.md / train_en.md 强化协议提醒(P0)
67+
- attach_file 描述去反向暗示
68+
- UI 现代化计划中 7 处图标尺寸令牌化
69+
- 发布身份迁移(publisher 与 Edge key,高风险,需独立评估)
70+
- vsce publish 的 Azure 认证路径未实测(msal 7 override 只验证了 package 路径)
71+
72+
## 4. 图标替换评估结论
73+
74+
- `doc/assets/webcode-logo-v2.png` 是 512x512 的 24bpp RGB,无 alpha 通道,四角不透明近黑色 #030710
75+
- VS Code 扩展与 Edge 扩展均要求透明背景 PNG,v2 不能直接使用
76+
- 需要用户提供带透明背景的源文件,或用现有 v2 抠底派生(需确认深色是否只用于背景)
77+
- 换图标只能实现视觉区分;发布身份区分需改 publisher 并重新生成 Edge key
78+
79+
## 5. 上下文恢复建议
80+
81+
新会话开始后,按以下顺序接手:
82+
83+
1. 跑 bridge-browser 编译与测试,确认协议漂移检测的落盘质量
84+
2. 修掉编译或测试错误后提交
85+
3. 落 train prompt 强化
86+
4. 视情况做 attach_file 描述修整
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as vscode from 'vscode';
2+
3+
export function getWorkspaceRoots(): string[] {
4+
return (vscode.workspace.workspaceFolders ?? []).map(folder => folder.uri.fsPath);
5+
}
6+
7+
export function getPrimaryWorkspaceRoot(): string | null {
8+
return getWorkspaceRoots()[0] ?? null;
9+
}

‎gateway-vscode/src/gateway.ts‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { BridgeSessionManager, DEFAULT_BRIDGE_CODE_TTL_MS } from './gateway/brid
2323
import { registerConfigRoutes } from './gateway/initRoutes';
2424
import type { InFlightToolCall } from './extension/shutdownSnapshot';
2525
import { seedSessionToken, type SessionTokenSource } from './extension/sessionToken';
26+
import { getPrimaryWorkspaceRoot, getWorkspaceRoots } from './extension/workspaceRoots';
2627
import { InFlightCallRegistry } from './gateway/inFlightCalls';
2728
import { DEFAULT_MAX_CONCURRENT_TOOL_CALLS, ToolConcurrencyGate } from './gateway/toolConcurrency';
2829
import { DEFAULT_IDLE_TIMEOUT_MINUTES, formatIdleTimeoutMinutes } from './gateway/idleTimeout';
@@ -237,13 +238,10 @@ export class GatewayManager {
237238
this.skillManager.invalidateCache(reason);
238239
}
239240

240-
private getPrimaryWorkspaceRoot(): string | null {
241-
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? null;
242-
}
243-
244241
private createToolExecutionContext(): ToolExecutionContext {
245242
return {
246-
workspaceRoot: this.getPrimaryWorkspaceRoot(),
243+
workspaceRoot: getPrimaryWorkspaceRoot(),
244+
workspaceRoots: getWorkspaceRoots(),
247245
outputChannel: this.outputChannel,
248246
skillManager: this.skillManager,
249247
terminalSessionManager: this.terminalSessionManager,
@@ -267,7 +265,7 @@ export class GatewayManager {
267265
error: this.error.bind(this),
268266
servers,
269267
toolRouter: this.toolRouter,
270-
workspaceRoot: this.getPrimaryWorkspaceRoot()
268+
workspaceRoot: getPrimaryWorkspaceRoot()
271269
});
272270
}
273271

@@ -307,7 +305,7 @@ export class GatewayManager {
307305
getGatewayInstanceId: () => this.gatewayInstanceId,
308306
getIdleTimeoutMs: () => this.idleTimeoutMs,
309307
getBridgeRequestMs: () => this.bridgeRequestMs,
310-
getWorkspaceRoot: () => this.getPrimaryWorkspaceRoot(),
308+
getWorkspaceRoot: () => getPrimaryWorkspaceRoot(),
311309
log: this.log.bind(this),
312310
recordActivity: () => this.resetWatchdog(),
313311
onSessionActivated: () => this.onBridgeSessionActivated?.()
@@ -320,7 +318,7 @@ export class GatewayManager {
320318
getAiSites: () => this.currentAiSites,
321319
getBridgeCodeTtlMs: () => this.bridgeSessionManager.getBridgeCodeTtlMs(),
322320
getPort: () => this.currentPort,
323-
getWorkspaceRoot: () => this.getPrimaryWorkspaceRoot(),
321+
getWorkspaceRoot: () => getPrimaryWorkspaceRoot(),
324322
hasActiveSession: () => this.bridgeSessionManager.hasActiveSession(),
325323
issueBridgeCode: (siteId, targetUrl) => this.issueBridgeCode(siteId, targetUrl),
326324
requestUserConfirmation: prompt => this.onSessionRequestConfirmation
@@ -351,7 +349,7 @@ export class GatewayManager {
351349
error: this.error.bind(this),
352350
getToolCallTimeoutMs: () => this.toolCallMs,
353351
getToolDefinition: (name: string) => this.getToolDefinition(name),
354-
getWorkspaceRoot: () => this.getPrimaryWorkspaceRoot(),
352+
getWorkspaceRoot: () => getPrimaryWorkspaceRoot(),
355353
localTools: this.localTools,
356354
log: this.log.bind(this),
357355
onToolCallEnd: callId => this.inFlightCalls.end(callId),

‎gateway-vscode/src/tools/attachFileTool.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
22
import * as path from 'path';
33
import type { LocalTool } from './types';
44
import { errorResult } from './result';
5-
import { WORKSPACE_FILE_PATH_DESCRIPTION, resolveWorkspaceRelativePath } from './workspacePath';
5+
import { WORKSPACE_FILE_PATH_DESCRIPTION, getContextWorkspaceRoots, resolveWorkspaceRelativePath } from './workspacePath';
66
import { assessSensitiveFileRisk, getSensitiveFileApprovalError } from '../servers/sensitivePathPolicy';
77
import {
88
ATTACH_FILE_MAX_BYTES,
@@ -34,7 +34,7 @@ export const attachFileTool: LocalTool = {
3434
const risk = assessSensitiveFileRisk('attach_file', args);
3535
if (getSensitiveFileApprovalError(risk, context.approvedCommandFingerprint)) { return errorResult(`Approval Required: ${risk.assessment.reasons.join(' ')}`); }
3636

37-
const resolved = await resolveWorkspaceRelativePath(context.workspaceRoot, args.path);
37+
const resolved = await resolveWorkspaceRelativePath(getContextWorkspaceRoots(context), args.path);
3838
const stats = await fs.stat(resolved.absolutePath);
3939
if (!stats.isFile()) {
4040
throw new Error(`path must point to a file: ${resolved.relativePath}`);

‎gateway-vscode/src/tools/editFileTool.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
createUnifiedDiff,
77
normalizeLineEndings
88
} from './filesystemUtils';
9-
import { WORKSPACE_FILE_PATH_DESCRIPTION, resolveWorkspaceRelativePath } from './workspacePath';
9+
import { WORKSPACE_FILE_PATH_DESCRIPTION, getContextWorkspaceRoots, resolveWorkspaceRelativePath } from './workspacePath';
1010
import { assessSensitiveFileRisk, getSensitiveFileApprovalError } from '../servers/sensitivePathPolicy';
1111

1212
type FileEdit = {
@@ -99,7 +99,7 @@ export const editFileTool: LocalTool = {
9999
const risk = assessSensitiveFileRisk('edit_file', args);
100100
if (getSensitiveFileApprovalError(risk, context.approvedCommandFingerprint)) { return errorResult(`Approval Required: ${risk.assessment.reasons.join(' ')}`); }
101101

102-
const filePath = (await resolveWorkspaceRelativePath(context.workspaceRoot, args.path)).absolutePath;
102+
const filePath = (await resolveWorkspaceRelativePath(getContextWorkspaceRoots(context), args.path)).absolutePath;
103103
const dryRun = args.dryRun === true;
104104
const rawContent = await fs.readFile(filePath, 'utf8');
105105
const usesCRLF = rawContent.includes('\r\n');

‎gateway-vscode/src/tools/executeCommandTool.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { execFile, type ChildProcess } from 'child_process';
22
import type { LocalTool, ToolDefinition, ToolExecutionContext, ToolRiskPreflight } from './types';
33
import { errorResult, jsonResult } from './result';
4-
import { WORKSPACE_COMMAND_PATH_DESCRIPTION, resolveWorkspaceRelativeDirectory } from './workspacePath';
4+
import { WORKSPACE_COMMAND_PATH_DESCRIPTION, getContextWorkspaceRoots, resolveWorkspaceRelativeDirectory } from './workspacePath';
55
import { normalizeShellCommand } from '../servers/commandShell';
66
import { formatCommandRiskAssessment } from '../servers/commandRisk';
77
import { getErrorMessage } from '../gateway/errorUtils';
@@ -138,7 +138,7 @@ async function resolveExecuteCommandRequest(
138138
}
139139

140140
const commandLine = normalizeShellCommand(args.command);
141-
const directory = await resolveWorkspaceRelativeDirectory(context.workspaceRoot, args.path ?? '.');
141+
const directory = await resolveWorkspaceRelativeDirectory(getContextWorkspaceRoots(context), args.path ?? '.');
142142
const execution = resolveBackgroundShellExecutionPlan(commandLine, args.profile, {
143143
platform: process.platform,
144144
env: process.env,

‎gateway-vscode/src/tools/readFileTool.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
getLineSelectionOptions,
1313
hasLineSelection
1414
} from './readFileLineSelection';
15-
import { WORKSPACE_FILE_PATH_DESCRIPTION, resolveWorkspaceRelativePath } from './workspacePath';
15+
import { WORKSPACE_FILE_PATH_DESCRIPTION, getContextWorkspaceRoots, resolveWorkspaceRelativePath } from './workspacePath';
1616
import { assessSensitiveFileRisk, getSensitiveFileApprovalError } from '../servers/sensitivePathPolicy';
1717
import { getAbortReasonText } from './abortUtils';
1818
import {
@@ -70,7 +70,7 @@ export const readFileTool: LocalTool = {
7070
const risk = assessSensitiveFileRisk('read_file', args);
7171
if (getSensitiveFileApprovalError(risk, context.approvedCommandFingerprint)) { return errorResult(`Approval Required: ${risk.assessment.reasons.join(' ')}`); }
7272

73-
const resolved = await resolveWorkspaceRelativePath(context.workspaceRoot, args.path);
73+
const resolved = await resolveWorkspaceRelativePath(getContextWorkspaceRoots(context), args.path);
7474
const fileStats = await fs.stat(resolved.absolutePath);
7575

7676
if (hasAttachmentExtension(resolved.relativePath)) {

‎gateway-vscode/src/tools/runInTerminalTool.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
ToolRiskPreflight
66
} from './types';
77
import { errorResult, jsonResult } from './result';
8-
import { WORKSPACE_COMMAND_PATH_DESCRIPTION, resolveWorkspaceRelativeDirectory } from './workspacePath';
8+
import { WORKSPACE_COMMAND_PATH_DESCRIPTION, getContextWorkspaceRoots, resolveWorkspaceRelativeDirectory } from './workspacePath';
99
import { normalizeShellCommand } from '../servers/commandShell';
1010
import { formatCommandRiskAssessment } from '../servers/commandRisk';
1111
import {
@@ -105,7 +105,7 @@ async function resolveRunInTerminalRequest(
105105
env: process.env,
106106
configuredCommandShellPath: context.commandShellPath
107107
});
108-
const directory = await resolveWorkspaceRelativeDirectory(context.workspaceRoot, args.path ?? '.');
108+
const directory = await resolveWorkspaceRelativeDirectory(getContextWorkspaceRoots(context), args.path ?? '.');
109109
const risk = assessCommandToolRisk({
110110
command: commandLine,
111111
commandPath: directory.relativePath,

‎gateway-vscode/src/tools/searchCodeTool.ts‎

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { spawn } from 'child_process';
2+
import * as path from 'path';
23
import type * as vscode from 'vscode';
3-
import type { LocalTool } from './types';
4+
import type { LocalTool, ToolExecutionContext } from './types';
45
import { textResult } from './result';
56
import { getNumberArg } from './filesystemUtils';
67
import { createSearchCodeFallbackNotice, searchCodeInProcess } from './searchCodeFallback';
@@ -18,7 +19,7 @@ import {
1819
MIN_MATCH_LINE_MAX_CHARS,
1920
} from './searchCodeUtils';
2021
import type { SearchCodeOptions } from './searchCodeTypes';
21-
import { WORKSPACE_SEARCH_PATH_DESCRIPTION, resolveWorkspaceRelativeDirectory } from './workspacePath';
22+
import { WORKSPACE_SEARCH_PATH_DESCRIPTION, getContextWorkspaceRoots, isDefaultSearchPath, resolveWorkspaceRelativeDirectory, toWorkspaceRootList } from './workspacePath';
2223

2324
export const searchCodeTool: LocalTool = {
2425
serverId: 'internal',
@@ -83,26 +84,77 @@ export const searchCodeTool: LocalTool = {
8384
if (signal?.aborted) {
8485
throw new Error(`search_code aborted: ${getAbortReasonText(signal)}`);
8586
}
86-
const searchRoot = (await resolveWorkspaceRelativeDirectory(context.workspaceRoot, args.path ?? '.')).absolutePath;
87+
const roots = toWorkspaceRootList(getContextWorkspaceRoots(context));
88+
if (roots.length > 1 && isDefaultSearchPath(args.path)) {
89+
const result = await searchCodeAcrossRoots(args, roots, context, signal);
90+
return textResult(formatSearchCodeOutput(result, buildSearchCodeOptions(args, roots[0], roots[0])));
91+
}
92+
const searchRoot = (await resolveWorkspaceRelativeDirectory(getContextWorkspaceRoots(context), args.path ?? '.')).absolutePath;
8793
const workspaceRoot = context.workspaceRoot ?? searchRoot;
88-
const query = String(args.query);
89-
const maxResults = getNumberArg(args.max_results, 100);
90-
const options = {
91-
searchRoot,
92-
workspaceRoot,
93-
query,
94-
maxResults,
95-
includePattern: typeof args.include === 'string' ? args.include : undefined,
96-
caseSensitive: args.case_sensitive === true,
97-
useRegex: getSearchCodeMatchMode(args.match) === 'regex',
98-
matchLineMaxChars: getBoundedSearchLineMaxChars(args.max_line_chars)
99-
};
94+
const options = buildSearchCodeOptions(args, searchRoot, workspaceRoot);
10095
const result = await runRipgrepWithFallback(options, context.outputChannel, signal);
10196

10297
return textResult(formatSearchCodeOutput(result, options));
10398
}
10499
};
105100

101+
function buildSearchCodeOptions(
102+
args: Record<string, unknown>,
103+
searchRoot: string,
104+
workspaceRoot: string
105+
): SearchCodeOptions {
106+
return {
107+
searchRoot,
108+
workspaceRoot,
109+
query: String(args.query),
110+
maxResults: getNumberArg(args.max_results, 100),
111+
includePattern: typeof args.include === 'string' ? args.include : undefined,
112+
caseSensitive: args.case_sensitive === true,
113+
useRegex: getSearchCodeMatchMode(args.match) === 'regex',
114+
matchLineMaxChars: getBoundedSearchLineMaxChars(args.max_line_chars)
115+
};
116+
}
117+
118+
/**
119+
* Default whole-workspace search in a multi-root workspace: one pass per
120+
* folder, each match line prefixed with the folder name so paths stay
121+
* unambiguous ("micro-server/src/app.ts:12: ..."). max_results is shared
122+
* across folders.
123+
*/
124+
async function searchCodeAcrossRoots(
125+
args: Record<string, unknown>,
126+
roots: string[],
127+
context: ToolExecutionContext,
128+
signal?: AbortSignal
129+
): Promise<SearchCodeResult> {
130+
const maxResults = getNumberArg(args.max_results, 100);
131+
const matches: string[] = [];
132+
const notices: string[] = [];
133+
let limited = false;
134+
135+
for (const root of roots) {
136+
if (matches.length >= maxResults) {
137+
limited = true;
138+
break;
139+
}
140+
const options = buildSearchCodeOptions(args, root, root);
141+
options.maxResults = maxResults - matches.length;
142+
const result = await runRipgrepWithFallback(options, context.outputChannel, signal);
143+
const prefix = `${path.basename(root)}/`;
144+
for (const match of result.matches) {
145+
matches.push(prefix + match);
146+
}
147+
for (const notice of result.notices ?? []) {
148+
if (!notices.includes(notice)) {
149+
notices.push(notice);
150+
}
151+
}
152+
limited = limited || result.limited;
153+
}
154+
155+
return { matches, limited, notices };
156+
}
157+
106158
async function runRipgrepWithFallback(
107159
options: SearchCodeOptions,
108160
outputChannel: vscode.OutputChannel,

0 commit comments

Comments
 (0)