Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionContentBuilder.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as pathLib from 'path';
7
import * as vscode from 'vscode';
8
import { ChatRequestTurn, ChatRequestTurn2, ChatResponseMarkdownPart, ChatResponseMultiDiffPart, ChatResponseProgressPart, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatResult, ChatToolInvocationPart, MarkdownString, Uri } from 'vscode';
9
import { IGitService } from '../../../platform/git/common/gitService';
10
import { PullRequestSearchItem, SessionInfo } from '../../../platform/github/common/githubAPI';
11
import { getAuthorDisplayName } from '../vscode/copilotCodingAgentUtils';
12
13
export interface SessionResponseLogChunk {
14
choices: Array<{
15
finish_reason?: 'tool_calls' | 'null' | (string & {});
16
delta: {
17
content?: string;
18
role: 'assistant' | (string & {});
19
tool_calls?: Array<{
20
function: {
21
arguments: string;
22
name: string;
23
};
24
id: string;
25
type: string;
26
index: number;
27
}>;
28
};
29
}>;
30
created: number;
31
id: string;
32
usage: {
33
completion_tokens: number;
34
prompt_tokens: number;
35
prompt_tokens_details: {
36
cached_tokens: number;
37
};
38
total_tokens: number;
39
};
40
model: string;
41
object: string;
42
}
43
44
export interface ToolCall {
45
function: {
46
arguments: string;
47
name: 'bash' | 'reply_to_comment' | (string & {});
48
};
49
id: string;
50
type: string;
51
index: number;
52
}
53
54
export interface AssistantDelta {
55
content?: string;
56
role: 'assistant' | (string & {});
57
tool_calls?: ToolCall[];
58
}
59
60
export interface Choice {
61
finish_reason?: 'tool_calls' | (string & {});
62
delta: {
63
content?: string;
64
role: 'assistant' | (string & {});
65
tool_calls?: ToolCall[];
66
};
67
}
68
69
export interface StrReplaceEditorToolData {
70
command: 'view' | 'edit' | string;
71
filePath?: string;
72
fileLabel?: string;
73
parsedContent?: { content: string; fileA: string | undefined; fileB: string | undefined };
74
viewRange?: { start: number; end: number };
75
}
76
77
export namespace StrReplaceEditorToolData {
78
export function is(value: any): value is StrReplaceEditorToolData {
79
return value && (typeof value.command === 'string');
80
}
81
}
82
83
export interface BashToolData {
84
commandLine: {
85
original: string;
86
};
87
language: 'bash';
88
}
89
90
export interface ParsedToolCallDetails {
91
toolName: string;
92
invocationMessage: string;
93
pastTenseMessage?: string;
94
originMessage?: string;
95
toolSpecificData?: StrReplaceEditorToolData | BashToolData;
96
}
97
98
export class ChatSessionContentBuilder {
99
constructor(
100
private type: string,
101
@IGitService private readonly _gitService: IGitService
102
) {
103
}
104
105
public async buildSessionHistory(
106
problemStatementPromise: Promise<string | undefined>,
107
sessions: SessionInfo[],
108
pullRequest: PullRequestSearchItem,
109
getLogsForSession: (id: string) => Promise<string>,
110
initialReferences: Promise<vscode.ChatPromptReference[]>,
111
): Promise<Array<ChatRequestTurn | ChatResponseTurn2>> {
112
const history: Array<ChatRequestTurn | ChatResponseTurn2> = [];
113
114
// Process all sessions concurrently and assemble results in order
115
const sessionResults = await Promise.all(
116
sessions.map(async (session, sessionIndex) => {
117
const [logs, problemStatement] = await Promise.all([getLogsForSession(session.id), sessionIndex === 0 ? problemStatementPromise : Promise.resolve(undefined)]);
118
119
const turns: Array<ChatRequestTurn | ChatResponseTurn2> = [];
120
121
// Create request turn with references for the first session
122
const references = sessionIndex === 0 ? Array.from(await initialReferences) : [];
123
turns.push(new ChatRequestTurn2(
124
problemStatement || session.name,
125
undefined, // command
126
references, // references
127
this.type,
128
[], // toolReferences
129
[],
130
undefined,
131
undefined,
132
undefined
133
));
134
135
// Create the PR card right after problem statement for first session
136
if (sessionIndex === 0 && pullRequest.author && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
137
const plaintextBody = pullRequest.body;
138
139
const card = new vscode.ChatResponsePullRequestPart({ command: 'github.copilot.chat.openPullRequestReroute', title: vscode.l10n.t('View Pull Request {0}', `#${pullRequest.number}`), arguments: [pullRequest.number] }, pullRequest.title, plaintextBody, getAuthorDisplayName(pullRequest.author), `#${pullRequest.number}`);
140
const cardTurn = new vscode.ChatResponseTurn2([card], {}, this.type);
141
turns.push(cardTurn);
142
}
143
144
const response = await this.createResponseTurn(pullRequest, logs, session);
145
if (response) {
146
turns.push(response);
147
}
148
149
return { sessionIndex, turns };
150
})
151
);
152
153
// Assemble results in correct order
154
sessionResults
155
.sort((a, b) => a.sessionIndex - b.sessionIndex)
156
.forEach(result => history.push(...result.turns));
157
158
return history;
159
}
160
161
private async createResponseTurn(pullRequest: PullRequestSearchItem, logs: string, session: SessionInfo): Promise<ChatResponseTurn2 | undefined> {
162
if (logs.trim().length > 0) {
163
return await this.parseSessionLogsIntoResponseTurn(pullRequest, logs, session);
164
} else if (session.state === 'in_progress' || session.state === 'queued') {
165
// For in-progress sessions without logs, create a placeholder response
166
const placeholderParts = [new ChatResponseProgressPart('Session is initializing...')];
167
const responseResult: ChatResult = {};
168
return new ChatResponseTurn2(placeholderParts, responseResult, this.type);
169
} else {
170
// For completed sessions without logs, add an empty response to maintain pairing
171
const emptyParts = [new ChatResponseMarkdownPart('_No logs available for this session_')];
172
const responseResult: ChatResult = {};
173
return new ChatResponseTurn2(emptyParts, responseResult, this.type);
174
}
175
}
176
177
private async parseSessionLogsIntoResponseTurn(pullRequest: PullRequestSearchItem, logs: string, session: SessionInfo): Promise<ChatResponseTurn2 | undefined> {
178
try {
179
const logChunks = this.parseSessionLogs(logs);
180
const responseParts: Array<ChatResponseMarkdownPart | ChatToolInvocationPart | ChatResponseMultiDiffPart> = [];
181
182
for (const chunk of logChunks) {
183
if (!chunk.choices || !Array.isArray(chunk.choices)) {
184
continue;
185
}
186
187
for (const choice of chunk.choices) {
188
const delta = choice.delta;
189
if (delta.role === 'assistant') {
190
this.processAssistantDelta(delta, choice, pullRequest, responseParts);
191
}
192
193
}
194
}
195
196
if (responseParts.length > 0) {
197
const responseResult: ChatResult = {};
198
return new ChatResponseTurn2(responseParts, responseResult, this.type);
199
}
200
201
return undefined;
202
} catch (error) {
203
return undefined;
204
}
205
}
206
207
public parseSessionLogs(rawText: string): SessionResponseLogChunk[] {
208
const parts = rawText
209
.split(/\r?\n/)
210
.filter(part => part.startsWith('data: '))
211
.map(part => part.slice('data: '.length).trim())
212
.map(part => JSON.parse(part));
213
214
return parts as SessionResponseLogChunk[];
215
}
216
217
private processAssistantDelta(
218
delta: AssistantDelta,
219
choice: Choice,
220
pullRequest: PullRequestSearchItem,
221
responseParts: Array<ChatResponseMarkdownPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart>,
222
): string {
223
let currentResponseContent = '';
224
if (delta.role === 'assistant') {
225
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : undefined;
226
// Handle special case for run_custom_setup_step
227
if (
228
choice.finish_reason === 'tool_calls' &&
229
toolCalls?.length &&
230
(toolCalls[0].function.name === 'run_custom_setup_step' || toolCalls[0].function.name === 'run_setup')
231
) {
232
const toolCall = toolCalls[0];
233
let args: { name?: string } = {};
234
try {
235
args = JSON.parse(toolCall.function.arguments);
236
} catch {
237
// fallback to empty args
238
}
239
240
if (delta.content && delta.content.trim()) {
241
const toolPart = this.createToolInvocationPart(pullRequest, toolCall, args.name || delta.content);
242
if (toolPart) {
243
responseParts.push(toolPart);
244
if (toolPart instanceof ChatResponseThinkingProgressPart) {
245
responseParts.push(new ChatResponseThinkingProgressPart('', '', { vscodeReasoningDone: true }));
246
}
247
}
248
}
249
// Skip if content is empty (running state)
250
} else {
251
if (delta.content) {
252
if (!delta.content.startsWith('<pr_title>') && !delta.content.startsWith('<error>')) {
253
currentResponseContent += delta.content;
254
}
255
}
256
257
const isError = delta.content?.startsWith('<error>');
258
if (toolCalls) {
259
// Add any accumulated content as markdown first
260
if (currentResponseContent.trim()) {
261
responseParts.push(new ChatResponseMarkdownPart(currentResponseContent.trim()));
262
currentResponseContent = '';
263
}
264
265
for (const toolCall of toolCalls) {
266
const toolPart = this.createToolInvocationPart(pullRequest, toolCall, delta.content || '');
267
if (toolPart) {
268
responseParts.push(toolPart);
269
if (toolPart instanceof ChatResponseThinkingProgressPart) {
270
responseParts.push(new ChatResponseThinkingProgressPart('', '', { vscodeReasoningDone: true }));
271
}
272
}
273
}
274
275
if (isError) {
276
const toolPart = new ChatToolInvocationPart('Command', 'command');
277
// Remove <error> at the start and </error> at the end
278
const cleaned = (delta.content ?? '').replace(/^\s*<error>\s*/i, '').replace(/\s*<\/error>\s*$/i, '');
279
toolPart.invocationMessage = cleaned;
280
toolPart.isError = true;
281
responseParts.push(toolPart);
282
}
283
} else {
284
const trimmedContent = currentResponseContent.trim();
285
if (trimmedContent) {
286
// TODO@rebornix @osortega validate if this is the only finish_reason for session end.
287
if (choice.finish_reason === 'stop') {
288
responseParts.push(new ChatResponseMarkdownPart(trimmedContent));
289
} else {
290
responseParts.push(new ChatResponseThinkingProgressPart(trimmedContent, '', { vscodeReasoningDone: true }));
291
}
292
currentResponseContent = '';
293
}
294
}
295
}
296
}
297
return currentResponseContent;
298
}
299
300
public createToolInvocationPart(pullRequest: PullRequestSearchItem, toolCall: ToolCall, deltaContent: string = ''): ChatToolInvocationPart | ChatResponseThinkingProgressPart | undefined {
301
if (!toolCall.function?.name || !toolCall.id) {
302
return undefined;
303
}
304
305
// Hide reply_to_comment tool
306
if (toolCall.function.name === 'reply_to_comment') {
307
return undefined;
308
}
309
310
const toolPart = new ChatToolInvocationPart(toolCall.function.name, toolCall.id);
311
toolPart.isComplete = true;
312
toolPart.isError = false;
313
toolPart.isConfirmed = true;
314
315
try {
316
const toolDetails = this.parseToolCallDetails(toolCall, deltaContent);
317
toolPart.toolName = toolDetails.toolName;
318
319
if (toolPart.toolName === 'think') {
320
return new ChatResponseThinkingProgressPart(toolDetails.invocationMessage);
321
}
322
323
if (toolCall.function.name === 'bash') {
324
toolPart.invocationMessage = new MarkdownString(`\`\`\`bash\n${toolDetails.invocationMessage}\n\`\`\``);
325
} else {
326
toolPart.invocationMessage = new MarkdownString(toolDetails.invocationMessage);
327
}
328
329
if (toolDetails.pastTenseMessage) {
330
toolPart.pastTenseMessage = new MarkdownString(toolDetails.pastTenseMessage);
331
}
332
if (toolDetails.originMessage) {
333
toolPart.originMessage = new MarkdownString(toolDetails.originMessage);
334
}
335
if (toolDetails.toolSpecificData) {
336
if (StrReplaceEditorToolData.is(toolDetails.toolSpecificData)) {
337
if ((toolDetails.toolSpecificData.command === 'view' || toolDetails.toolSpecificData.command === 'edit') && toolDetails.toolSpecificData.fileLabel) {
338
const currentRepository = this._gitService.activeRepository.get();
339
const uri = currentRepository?.rootUri ? Uri.file(pathLib.join(currentRepository.rootUri.fsPath, toolDetails.toolSpecificData.fileLabel)) : Uri.file(toolDetails.toolSpecificData.fileLabel);
340
toolPart.invocationMessage = new MarkdownString(`${toolPart.toolName} [](${uri.toString()})` + (toolDetails.toolSpecificData?.viewRange ? `, lines ${toolDetails.toolSpecificData.viewRange?.start} to ${toolDetails.toolSpecificData.viewRange?.end}` : ''));
341
toolPart.invocationMessage.supportHtml = true;
342
toolPart.pastTenseMessage = new MarkdownString(`${toolPart.toolName} [](${uri.toString()})` + (toolDetails.toolSpecificData?.viewRange ? `, lines ${toolDetails.toolSpecificData.viewRange?.start} to ${toolDetails.toolSpecificData.viewRange?.end}` : ''));
343
}
344
} else {
345
toolPart.toolSpecificData = toolDetails.toolSpecificData;
346
}
347
}
348
} catch (error) {
349
toolPart.toolName = toolCall.function.name || 'unknown';
350
toolPart.invocationMessage = new MarkdownString(`Tool: ${toolCall.function.name}`);
351
toolPart.isError = true;
352
}
353
354
return toolPart;
355
}
356
357
/**
358
* Convert absolute file path to relative file label
359
* File paths are absolute and look like: `/home/runner/work/repo/repo/<path>`
360
*/
361
private toFileLabel(file: string): string {
362
const parts = file.split('/');
363
return parts.slice(6).join('/');
364
}
365
366
private parseRange(view_range: unknown): { start: number; end: number } | undefined {
367
if (!view_range) {
368
return undefined;
369
}
370
371
if (!Array.isArray(view_range)) {
372
return undefined;
373
}
374
375
if (view_range.length !== 2) {
376
return undefined;
377
}
378
379
const start = view_range[0];
380
const end = view_range[1];
381
382
if (typeof start !== 'number' || typeof end !== 'number') {
383
return undefined;
384
}
385
386
return {
387
start,
388
end
389
};
390
}
391
392
/**
393
* Parse diff content and extract file information
394
*/
395
private parseDiff(content: string): { content: string; fileA: string | undefined; fileB: string | undefined } | undefined {
396
const lines = content.split(/\r?\n/g);
397
let fileA: string | undefined;
398
let fileB: string | undefined;
399
400
let startDiffLineIndex = -1;
401
for (let i = 0; i < lines.length; i++) {
402
const line = lines[i];
403
if (line.startsWith('diff --git')) {
404
const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
405
if (match) {
406
fileA = match[1];
407
fileB = match[2];
408
}
409
} else if (line.startsWith('@@ ')) {
410
startDiffLineIndex = i + 1;
411
break;
412
}
413
}
414
if (startDiffLineIndex < 0) {
415
return undefined;
416
}
417
418
return {
419
content: lines.slice(startDiffLineIndex).join('\n'),
420
fileA: typeof fileA === 'string' ? '/' + fileA : undefined,
421
fileB: typeof fileB === 'string' ? '/' + fileB : undefined
422
};
423
}
424
425
/**
426
* Parse tool call arguments and return normalized tool details
427
*/
428
private parseToolCallDetails(
429
toolCall: {
430
function: { name: string; arguments: string };
431
id: string;
432
type: string;
433
index: number;
434
},
435
content: string
436
): ParsedToolCallDetails {
437
// Parse arguments once with graceful fallback
438
let args: { command?: string; path?: string; prDescription?: string; commitMessage?: string; view_range?: unknown } = {};
439
try { args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {}; } catch { /* ignore */ }
440
441
const name = toolCall.function.name;
442
443
// Small focused helpers to remove duplication while preserving behavior
444
const buildReadDetails = (filePath: string | undefined, parsedRange: { start: number; end: number } | undefined, opts?: { parsedContent?: { content: string; fileA: string | undefined; fileB: string | undefined } }): ParsedToolCallDetails => {
445
const fileLabel = filePath && this.toFileLabel(filePath);
446
if (fileLabel === undefined || fileLabel === '') {
447
return { toolName: 'Read repository', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' };
448
}
449
const rangeSuffix = parsedRange ? `, lines ${parsedRange.start} to ${parsedRange.end}` : '';
450
// Default helper returns bracket variant (used for generic view). Plain variant handled separately for str_replace_editor non-diff.
451
return {
452
toolName: 'Read',
453
invocationMessage: `Read [](${fileLabel})${rangeSuffix}`,
454
pastTenseMessage: `Read [](${fileLabel})${rangeSuffix}`,
455
toolSpecificData: {
456
command: 'view',
457
filePath: filePath,
458
fileLabel: fileLabel,
459
parsedContent: opts?.parsedContent,
460
viewRange: parsedRange
461
}
462
};
463
};
464
465
const buildEditDetails = (filePath: string | undefined, command: string = 'edit', parsedRange: { start: number; end: number } | undefined, opts?: { defaultName?: string }): ParsedToolCallDetails => {
466
const fileLabel = filePath && this.toFileLabel(filePath);
467
const rangeSuffix = parsedRange ? `, lines ${parsedRange.start} to ${parsedRange.end}` : '';
468
let invocationMessage: string;
469
let pastTenseMessage: string;
470
if (fileLabel) {
471
invocationMessage = `Edit [](${fileLabel})${rangeSuffix}`;
472
pastTenseMessage = `Edit [](${fileLabel})${rangeSuffix}`;
473
} else {
474
if (opts?.defaultName === 'Create') {
475
invocationMessage = pastTenseMessage = `Create File ${filePath}`;
476
} else {
477
invocationMessage = pastTenseMessage = (opts?.defaultName || 'Edit');
478
}
479
invocationMessage += rangeSuffix;
480
pastTenseMessage += rangeSuffix;
481
}
482
483
return {
484
toolName: opts?.defaultName || 'Edit',
485
invocationMessage,
486
pastTenseMessage,
487
toolSpecificData: fileLabel ? {
488
command: command || (opts?.defaultName === 'Create' ? 'create' : (command || 'edit')),
489
filePath: filePath,
490
fileLabel: fileLabel,
491
viewRange: parsedRange
492
} : undefined
493
};
494
};
495
496
const buildStrReplaceDetails = (filePath: string | undefined): ParsedToolCallDetails => {
497
const fileLabel = filePath && this.toFileLabel(filePath);
498
const message = fileLabel ? `Edit [](${fileLabel})` : `Edit ${filePath}`;
499
return {
500
toolName: 'Edit',
501
invocationMessage: message,
502
pastTenseMessage: message,
503
toolSpecificData: fileLabel ? { command: 'str_replace', filePath, fileLabel } : undefined
504
};
505
};
506
507
const buildCreateDetails = (filePath: string | undefined): ParsedToolCallDetails => {
508
const fileLabel = filePath && this.toFileLabel(filePath);
509
const message = fileLabel ? `Create [](${fileLabel})` : `Create File ${filePath}`;
510
return {
511
toolName: 'Create',
512
invocationMessage: message,
513
pastTenseMessage: message,
514
toolSpecificData: fileLabel ? { command: 'create', filePath, fileLabel } : undefined
515
};
516
};
517
518
const buildBashDetails = (bashArgs: typeof args, contentStr: string): ParsedToolCallDetails => {
519
const command = bashArgs.command ? `$ ${bashArgs.command}` : undefined;
520
const bashContent = [command, contentStr].filter(Boolean).join('\n');
521
522
const MAX_CONTENT_LENGTH = 200;
523
let displayContent = bashContent;
524
if (bashContent && bashContent.length > MAX_CONTENT_LENGTH) {
525
// Check if content contains EOF marker (heredoc pattern)
526
const hasEOF = (bashContent && /<<\s*['"]?EOF['"]?/.test(bashContent));
527
if (hasEOF) {
528
// show the command line up to EOL
529
const firstLineEnd = bashContent.indexOf('\n');
530
if (firstLineEnd > 0) {
531
const firstLine = bashContent.substring(0, firstLineEnd);
532
const remainingChars = bashContent.length - firstLineEnd - 1;
533
displayContent = firstLine + `\n... [${remainingChars} characters of heredoc content]`;
534
} else {
535
displayContent = bashContent;
536
}
537
} else {
538
displayContent = bashContent.substring(0, MAX_CONTENT_LENGTH) + `\n... [${bashContent.length - MAX_CONTENT_LENGTH} more characters]`;
539
}
540
}
541
542
const details: ParsedToolCallDetails = { toolName: 'Run Bash command', invocationMessage: bashContent || 'Run Bash command' };
543
if (bashArgs.command) { details.toolSpecificData = { commandLine: { original: displayContent ?? '' }, language: 'bash' }; }
544
return details;
545
};
546
547
switch (name) {
548
case 'str_replace_editor': {
549
if (args.command === 'view') {
550
const parsedContent = this.parseDiff(content);
551
const parsedRange = this.parseRange(args.view_range);
552
if (parsedContent) {
553
const file = parsedContent.fileA ?? parsedContent.fileB;
554
const fileLabel = file && this.toFileLabel(file);
555
if (fileLabel === '') {
556
return { toolName: 'Read repository', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' };
557
} else if (fileLabel === undefined) {
558
return { toolName: 'Read', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' };
559
} else {
560
const rangeSuffix = parsedRange ? `, lines ${parsedRange.start} to ${parsedRange.end}` : '';
561
return {
562
toolName: 'Read',
563
invocationMessage: `Read [](${fileLabel})${rangeSuffix}`,
564
pastTenseMessage: `Read [](${fileLabel})${rangeSuffix}`,
565
toolSpecificData: { command: 'view', filePath: file, fileLabel, parsedContent, viewRange: parsedRange }
566
};
567
}
568
}
569
// No diff parsed: use PLAIN (non-bracket) variant for str_replace_editor views
570
const plainRange = this.parseRange(args.view_range);
571
const fp = args.path; const fl = fp && this.toFileLabel(fp);
572
if (fl === undefined || fl === '') {
573
return { toolName: 'Read repository', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' };
574
}
575
const suffix = plainRange ? `, lines ${plainRange.start} to ${plainRange.end}` : '';
576
return {
577
toolName: 'Read',
578
invocationMessage: `Read ${fl}${suffix}`,
579
pastTenseMessage: `Read ${fl}${suffix}`,
580
toolSpecificData: { command: 'view', filePath: fp, fileLabel: fl, viewRange: plainRange }
581
};
582
}
583
return buildEditDetails(args.path, args.command, this.parseRange(args.view_range));
584
}
585
case 'str_replace':
586
return buildStrReplaceDetails(args.path);
587
case 'create':
588
return buildCreateDetails(args.path);
589
case 'view':
590
return buildReadDetails(args.path, this.parseRange(args.view_range)); // generic view always bracket variant
591
case 'think': {
592
const thought = (args as unknown as { thought?: string }).thought || content || 'Thought';
593
return { toolName: 'think', invocationMessage: thought };
594
}
595
case 'report_progress': {
596
const details: ParsedToolCallDetails = { toolName: 'Progress Update', invocationMessage: `${args.prDescription}` || content || 'Progress Update' };
597
if (args.commitMessage) { details.originMessage = `Commit: ${args.commitMessage}`; }
598
return details;
599
}
600
case 'bash':
601
return buildBashDetails(args, content);
602
case 'read_bash':
603
return { toolName: 'read_bash', invocationMessage: 'Read logs from Bash session' };
604
case 'stop_bash':
605
return { toolName: 'stop_bash', invocationMessage: 'Stop Bash session' };
606
case 'edit':
607
return buildEditDetails(args.path, args.command, undefined);
608
default:
609
return { toolName: name || 'unknown', invocationMessage: content || name || 'unknown' };
610
}
611
}
612
}
613
614