Skip to content

Commit 6d3f10d

Browse files
author
catlog22
committed
feat: 增加文件读取功能的行分页支持,优化智能搜索的多词查询匹配
1 parent 09483c9 commit 6d3f10d

File tree

3 files changed

+238
-30
lines changed

3 files changed

+238
-30
lines changed

ccw/src/templates/dashboard-js/views/mcp-manager.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ async function renderMcpManager() {
256256
</div>
257257
<div class="grid grid-cols-1 gap-2">
258258
<div class="flex items-center gap-2">
259-
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_PROJECT_ROOT</label>
259+
<label class="text-xs text-muted-foreground w-36 shrink-0">CCW_PROJECT_ROOT</label>
260260
<input type="text"
261261
class="ccw-project-root-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
262262
placeholder="${projectPath || t('mcp.useCurrentDir')}"
@@ -268,7 +268,7 @@ async function renderMcpManager() {
268268
</button>
269269
</div>
270270
<div class="flex items-center gap-2">
271-
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_ALLOWED_DIRS</label>
271+
<label class="text-xs text-muted-foreground w-36 shrink-0">CCW_ALLOWED_DIRS</label>
272272
<input type="text"
273273
class="ccw-allowed-dirs-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
274274
placeholder="${t('mcp.allowedDirsPlaceholder')}"
@@ -470,7 +470,7 @@ async function renderMcpManager() {
470470
</div>
471471
<div class="grid grid-cols-1 gap-2">
472472
<div class="flex items-center gap-2">
473-
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_PROJECT_ROOT</label>
473+
<label class="text-xs text-muted-foreground w-36 shrink-0">CCW_PROJECT_ROOT</label>
474474
<input type="text"
475475
class="ccw-project-root-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
476476
placeholder="${projectPath || t('mcp.useCurrentDir')}"
@@ -482,7 +482,7 @@ async function renderMcpManager() {
482482
</button>
483483
</div>
484484
<div class="flex items-center gap-2">
485-
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_ALLOWED_DIRS</label>
485+
<label class="text-xs text-muted-foreground w-36 shrink-0">CCW_ALLOWED_DIRS</label>
486486
<input type="text"
487487
class="ccw-allowed-dirs-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
488488
placeholder="${t('mcp.allowedDirsPlaceholder')}"

ccw/src/tools/read-file.ts

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const ParamsSchema = z.object({
3030
maxDepth: z.number().default(3).describe('Max directory depth to traverse'),
3131
includeContent: z.boolean().default(true).describe('Include file content in result'),
3232
maxFiles: z.number().default(MAX_FILES).describe('Max number of files to return'),
33+
offset: z.number().min(0).optional().describe('Line offset to start reading from (0-based, for single file only)'),
34+
limit: z.number().min(1).optional().describe('Number of lines to read (for single file only)'),
3335
});
3436

3537
type Params = z.infer<typeof ParamsSchema>;
@@ -40,6 +42,8 @@ interface FileEntry {
4042
content?: string;
4143
truncated?: boolean;
4244
matches?: string[];
45+
totalLines?: number;
46+
lineRange?: { start: number; end: number };
4347
}
4448

4549
interface ReadResult {
@@ -123,23 +127,69 @@ function collectFiles(
123127
return files;
124128
}
125129

130+
interface ReadContentOptions {
131+
maxLength: number;
132+
offset?: number;
133+
limit?: number;
134+
}
135+
136+
interface ReadContentResult {
137+
content: string;
138+
truncated: boolean;
139+
totalLines?: number;
140+
lineRange?: { start: number; end: number };
141+
}
142+
126143
/**
127-
* Read file content with truncation
144+
* Read file content with truncation and optional line-based pagination
128145
*/
129-
function readFileContent(filePath: string, maxLength: number): { content: string; truncated: boolean } {
146+
function readFileContent(filePath: string, options: ReadContentOptions): ReadContentResult {
147+
const { maxLength, offset, limit } = options;
148+
130149
if (isBinaryFile(filePath)) {
131150
return { content: '[Binary file]', truncated: false };
132151
}
133152

134153
try {
135154
const content = readFileSync(filePath, 'utf8');
155+
const lines = content.split('\n');
156+
const totalLines = lines.length;
157+
158+
// If offset/limit specified, use line-based pagination
159+
if (offset !== undefined || limit !== undefined) {
160+
const startLine = Math.min(offset ?? 0, totalLines);
161+
const endLine = limit !== undefined ? Math.min(startLine + limit, totalLines) : totalLines;
162+
const selectedLines = lines.slice(startLine, endLine);
163+
const selectedContent = selectedLines.join('\n');
164+
165+
const actualEnd = endLine;
166+
const hasMore = actualEnd < totalLines;
167+
168+
let finalContent = selectedContent;
169+
if (selectedContent.length > maxLength) {
170+
finalContent = selectedContent.substring(0, maxLength) + `\n... (+${selectedContent.length - maxLength} chars)`;
171+
}
172+
173+
// Calculate actual line range (handle empty selection)
174+
const actualLineEnd = selectedLines.length > 0 ? startLine + selectedLines.length - 1 : startLine;
175+
176+
return {
177+
content: finalContent,
178+
truncated: hasMore || selectedContent.length > maxLength,
179+
totalLines,
180+
lineRange: { start: startLine, end: actualLineEnd },
181+
};
182+
}
183+
184+
// Default behavior: truncate by character length
136185
if (content.length > maxLength) {
137186
return {
138187
content: content.substring(0, maxLength) + `\n... (+${content.length - maxLength} chars)`,
139-
truncated: true
188+
truncated: true,
189+
totalLines,
140190
};
141191
}
142-
return { content, truncated: false };
192+
return { content, truncated: false, totalLines };
143193
} catch (error) {
144194
return { content: `[Error: ${(error as Error).message}]`, truncated: false };
145195
}
@@ -171,15 +221,17 @@ function findMatches(content: string, pattern: string): string[] {
171221
// Tool schema for MCP
172222
export const schema: ToolSchema = {
173223
name: 'read_file',
174-
description: `Read files with multi-file, directory, and regex support.
224+
description: `Read files with multi-file, directory, regex support, and line-based pagination.
175225
176226
Usage:
177-
read_file(paths="file.ts") # Single file
178-
read_file(paths=["a.ts", "b.ts"]) # Multiple files
179-
read_file(paths="src/", pattern="*.ts") # Directory with pattern
180-
read_file(paths="src/", contentPattern="TODO") # Search content
181-
182-
Returns compact file list with optional content.`,
227+
read_file(paths="file.ts") # Single file (full content)
228+
read_file(paths="file.ts", offset=100, limit=50) # Lines 100-149 (0-based)
229+
read_file(paths=["a.ts", "b.ts"]) # Multiple files
230+
read_file(paths="src/", pattern="*.ts") # Directory with pattern
231+
read_file(paths="src/", contentPattern="TODO") # Search content
232+
233+
Supports both absolute and relative paths. Relative paths are resolved from project root.
234+
Returns compact file list with optional content. Use offset/limit for large file pagination.`,
183235
inputSchema: {
184236
type: 'object',
185237
properties: {
@@ -213,6 +265,16 @@ Returns compact file list with optional content.`,
213265
description: `Max number of files to return (default: ${MAX_FILES})`,
214266
default: MAX_FILES,
215267
},
268+
offset: {
269+
type: 'number',
270+
description: 'Line offset to start reading from (0-based, for single file only)',
271+
minimum: 0,
272+
},
273+
limit: {
274+
type: 'number',
275+
description: 'Number of lines to read (for single file only)',
276+
minimum: 1,
277+
},
216278
},
217279
required: ['paths'],
218280
},
@@ -232,6 +294,8 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
232294
maxDepth,
233295
includeContent,
234296
maxFiles,
297+
offset,
298+
limit,
235299
} = parsed.data;
236300

237301
const cwd = getProjectRoot();
@@ -271,6 +335,10 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
271335
const files: FileEntry[] = [];
272336
let totalContent = 0;
273337

338+
// Only apply offset/limit for single file mode
339+
const isSingleFile = limitedFiles.length === 1;
340+
const useLinePagination = isSingleFile && (offset !== undefined || limit !== undefined);
341+
274342
for (const filePath of limitedFiles) {
275343
if (totalContent >= MAX_TOTAL_CONTENT) break;
276344

@@ -283,7 +351,15 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
283351
if (includeContent) {
284352
const remainingSpace = MAX_TOTAL_CONTENT - totalContent;
285353
const maxLen = Math.min(MAX_CONTENT_LENGTH, remainingSpace);
286-
const { content, truncated } = readFileContent(filePath, maxLen);
354+
355+
// Pass offset/limit only for single file mode
356+
const readOptions: ReadContentOptions = { maxLength: maxLen };
357+
if (useLinePagination) {
358+
if (offset !== undefined) readOptions.offset = offset;
359+
if (limit !== undefined) readOptions.limit = limit;
360+
}
361+
362+
const { content, truncated, totalLines, lineRange } = readFileContent(filePath, readOptions);
287363

288364
// If contentPattern provided, only include files with matches
289365
if (contentPattern) {
@@ -292,13 +368,17 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
292368
entry.matches = matches;
293369
entry.content = content;
294370
entry.truncated = truncated;
371+
entry.totalLines = totalLines;
372+
entry.lineRange = lineRange;
295373
totalContent += content.length;
296374
} else {
297375
continue; // Skip files without matches
298376
}
299377
} else {
300378
entry.content = content;
301379
entry.truncated = truncated;
380+
entry.totalLines = totalLines;
381+
entry.lineRange = lineRange;
302382
totalContent += content.length;
303383
}
304384
}
@@ -311,6 +391,10 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
311391
if (totalFiles > maxFiles) {
312392
message += ` (showing ${maxFiles} of ${totalFiles})`;
313393
}
394+
if (useLinePagination && files.length > 0 && files[0].lineRange) {
395+
const { start, end } = files[0].lineRange;
396+
message += ` [lines ${start}-${end} of ${files[0].totalLines}]`;
397+
}
314398
if (contentPattern) {
315399
message += ` matching "${contentPattern}"`;
316400
}

0 commit comments

Comments
 (0)