Skip to content

Commit 6f77a42

Browse files
committed
refactor and share types
1 parent f074e7f commit 6f77a42

File tree

6 files changed

+176
-192
lines changed

6 files changed

+176
-192
lines changed

common/sessionParsing.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
export interface SessionResponseLogChunk {
7+
choices: Array<{
8+
finish_reason: string;
9+
delta: {
10+
content?: string;
11+
role: string;
12+
tool_calls?: Array<{
13+
function: {
14+
arguments: string;
15+
name: string;
16+
};
17+
id: string;
18+
type: string;
19+
index: number;
20+
}>;
21+
};
22+
}>;
23+
created: number;
24+
id: string;
25+
usage: {
26+
completion_tokens: number;
27+
prompt_tokens: number;
28+
prompt_tokens_details: {
29+
cached_tokens: number;
30+
};
31+
total_tokens: number;
32+
};
33+
model: string;
34+
object: string;
35+
}
36+
37+
export interface ParsedToolCall {
38+
type: 'str_replace_editor' | 'think' | 'bash' | 'report_progress' | 'unknown';
39+
name: string;
40+
args: any;
41+
content: string;
42+
command?: string; // For str_replace_editor
43+
}
44+
45+
export interface ParsedChoice {
46+
type: 'assistant_content' | 'tool_call' | 'pr_title';
47+
content?: string;
48+
toolCall?: ParsedToolCall;
49+
finishReason?: string;
50+
}
51+
52+
export interface ParsedToolCallDetails {
53+
toolName: string;
54+
invocationMessage: string;
55+
pastTenseMessage?: string;
56+
originMessage?: string;
57+
toolSpecificData?: any;
58+
}
59+
60+
/**
61+
* Parse tool call arguments and return normalized tool details
62+
*/
63+
export function parseToolCallDetails(
64+
toolCall: {
65+
function: { name: string; arguments: string };
66+
id: string;
67+
type: string;
68+
index: number;
69+
},
70+
content: string
71+
): ParsedToolCallDetails {
72+
let args: any = {};
73+
try {
74+
args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {};
75+
} catch {
76+
// fallback to empty args
77+
}
78+
79+
const name = toolCall.function.name;
80+
81+
if (name === 'str_replace_editor') {
82+
if (args.command === 'view') {
83+
return {
84+
toolName: args.path ? `View ${args.path}` : 'View repository',
85+
invocationMessage: `View ${args.path}`,
86+
pastTenseMessage: `View ${args.path}`
87+
};
88+
} else {
89+
return {
90+
toolName: 'Edit',
91+
invocationMessage: `Edit: ${args.path}`,
92+
pastTenseMessage: `Edit: ${args.path}`
93+
};
94+
}
95+
} else if (name === 'think') {
96+
return {
97+
toolName: 'Thought',
98+
invocationMessage: content
99+
};
100+
} else if (name === 'report_progress') {
101+
const details: ParsedToolCallDetails = {
102+
toolName: 'Progress Update',
103+
invocationMessage: args.prDescription || content
104+
};
105+
if (args.commitMessage) {
106+
details.originMessage = `Commit: ${args.commitMessage}`;
107+
}
108+
return details;
109+
} else if (name === 'bash') {
110+
const command = args.command ? `$ ${args.command}` : undefined;
111+
const bashContent = [command, content].filter(Boolean).join('\n');
112+
const details: ParsedToolCallDetails = {
113+
toolName: 'Run Bash command',
114+
invocationMessage: bashContent
115+
};
116+
117+
// Use the terminal-specific data for bash commands
118+
if (args.command) {
119+
details.toolSpecificData = {
120+
command: args.command,
121+
language: 'bash'
122+
};
123+
}
124+
return details;
125+
} else {
126+
// Unknown tool type
127+
return {
128+
toolName: name || 'unknown',
129+
invocationMessage: content
130+
};
131+
}
132+
}
133+
134+
/**
135+
* Parse raw session logs text into structured log chunks
136+
*/
137+
export function parseSessionLogs(rawText: string): SessionResponseLogChunk[] {
138+
const parts = rawText
139+
.split(/\r?\n/)
140+
.filter(part => part.startsWith('data: '))
141+
.map(part => part.slice('data: '.length).trim())
142+
.map(part => JSON.parse(part));
143+
144+
return parts as SessionResponseLogChunk[];
145+
}

src/github/copilotRemoteAgent.ts

Lines changed: 22 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import vscode from 'vscode';
7+
import { parseSessionLogs, parseToolCallDetails } from '../../common/sessionParsing';
78
import { Repository } from '../api/api';
89
import { COPILOT_LOGINS, CopilotPRStatus } from '../common/copilot';
910
import { commands } from '../common/executeCommands';
@@ -900,49 +901,32 @@ export class CopilotRemoteAgentManager extends Disposable {
900901
toolPart.isError = false;
901902
toolPart.isConfirmed = true;
902903

903-
// Parse tool arguments and set tool-specific metadata following sessionView.tsx patterns
904+
// Parse tool arguments and set tool-specific metadata using shared utility
904905
const name = toolCall.function.name;
905906
const content = delta.content || '';
906907

907908
try {
908-
const args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {};
909-
910-
if (name === 'str_replace_editor') {
911-
if (args.command === 'view') {
912-
toolPart.toolName = args.path ? `View ${this.toFileLabel(args.path)}` : 'View repository';
913-
toolPart.invocationMessage = `View ${args.path}`;
914-
toolPart.pastTenseMessage = `View ${args.path}`;
915-
} else {
916-
toolPart.toolName = 'Edit';
917-
toolPart.invocationMessage = `Edit: ${args.path}`;
918-
toolPart.pastTenseMessage = `Edit: ${args.path}`;
919-
}
920-
} else if (name === 'think') {
921-
toolPart.toolName = 'Thought';
922-
toolPart.invocationMessage = content;
923-
} else if (name === 'report_progress') {
924-
toolPart.toolName = 'Progress Update';
925-
toolPart.invocationMessage = args.prDescription || content;
926-
if (args.commitMessage) {
927-
toolPart.originMessage = `Commit: ${args.commitMessage}`;
928-
}
929-
} else if (name === 'bash') {
930-
toolPart.toolName = 'Run Bash command';
931-
const command = args.command ? `$ ${args.command}` : undefined;
932-
const bashContent = [command, content].filter(Boolean).join('\n');
933-
toolPart.invocationMessage = new vscode.MarkdownString(`\`\`\`bash\n${bashContent}\n\`\`\``);
934-
935-
// Use the terminal-specific data for bash commands
936-
if (args.command) {
937-
toolPart.toolSpecificData = {
938-
command: args.command,
939-
language: 'bash'
940-
};
941-
}
909+
const toolDetails = parseToolCallDetails(toolCall, content);
910+
911+
toolPart.toolName = toolDetails.toolName;
912+
913+
// Handle different invocation message types
914+
if (name === 'bash') {
915+
toolPart.invocationMessage = new vscode.MarkdownString(`\`\`\`bash\n${toolDetails.invocationMessage}\n\`\`\``);
942916
} else {
943-
// Unknown tool type
944-
toolPart.toolName = name || 'unknown';
945-
toolPart.invocationMessage = new vscode.MarkdownString(`\`\`\`plaintext\n${content}\n\`\`\``);
917+
toolPart.invocationMessage = toolDetails.invocationMessage;
918+
}
919+
920+
if (toolDetails.pastTenseMessage) {
921+
toolPart.pastTenseMessage = toolDetails.pastTenseMessage;
922+
}
923+
924+
if (toolDetails.originMessage) {
925+
toolPart.originMessage = toolDetails.originMessage;
926+
}
927+
928+
if (toolDetails.toolSpecificData) {
929+
toolPart.toolSpecificData = toolDetails.toolSpecificData;
946930
}
947931
} catch (error) {
948932
// Fallback for parsing errors
@@ -987,111 +971,4 @@ export class CopilotRemoteAgentManager extends Disposable {
987971
return [];
988972
}
989973
}
990-
991-
/**
992-
* Helper method to convert absolute file paths to relative labels
993-
* Following the pattern from sessionView.tsx
994-
*/
995-
private toFileLabel(file: string): string {
996-
// File paths are absolute and look like: `/home/runner/work/repo/repo/<path>`
997-
const parts = file.split('/');
998-
return parts.slice(6).join('/');
999-
}
1000-
1001-
/**
1002-
* Helper method to get language for a file based on its extension
1003-
* Following the pattern from sessionView.tsx
1004-
*/
1005-
private getLanguageForFile(filePath: string): string {
1006-
const extension = filePath.split('.').pop()?.toLowerCase();
1007-
1008-
// Common language mappings
1009-
const languageMap: { [ext: string]: string } = {
1010-
'ts': 'typescript',
1011-
'tsx': 'tsx',
1012-
'js': 'javascript',
1013-
'jsx': 'jsx',
1014-
'py': 'python',
1015-
'json': 'json',
1016-
'md': 'markdown',
1017-
'yml': 'yaml',
1018-
'yaml': 'yaml',
1019-
'xml': 'xml',
1020-
'html': 'html',
1021-
'css': 'css',
1022-
'scss': 'scss',
1023-
'less': 'less',
1024-
'sh': 'bash',
1025-
'bash': 'bash',
1026-
'zsh': 'bash',
1027-
'fish': 'bash',
1028-
'ps1': 'powershell',
1029-
'sql': 'sql',
1030-
'go': 'go',
1031-
'rs': 'rust',
1032-
'cpp': 'cpp',
1033-
'c': 'c',
1034-
'h': 'c',
1035-
'hpp': 'cpp',
1036-
'java': 'java',
1037-
'kt': 'kotlin',
1038-
'swift': 'swift',
1039-
'rb': 'ruby',
1040-
'php': 'php',
1041-
'cs': 'csharp',
1042-
'fs': 'fsharp',
1043-
'vb': 'vb',
1044-
'r': 'r',
1045-
'scala': 'scala',
1046-
'clj': 'clojure',
1047-
'elm': 'elm',
1048-
'dart': 'dart',
1049-
'lua': 'lua',
1050-
'perl': 'perl',
1051-
'vim': 'vim'
1052-
};
1053-
1054-
return extension ? languageMap[extension] || 'plaintext' : 'plaintext';
1055-
}
1056-
}
1057-
1058-
function parseSessionLogs(rawText: string): SessionResponseLogChunk[] {
1059-
const parts = rawText
1060-
.split(/\r?\n/)
1061-
.filter(part => part.startsWith('data: '))
1062-
.map(part => part.slice('data: '.length).trim())
1063-
.map(part => JSON.parse(part));
1064-
1065-
return parts as SessionResponseLogChunk[];
1066-
}
1067-
1068-
export interface SessionResponseLogChunk {
1069-
choices: Array<{
1070-
finish_reason: string;
1071-
delta: {
1072-
content?: string;
1073-
role: string;
1074-
tool_calls?: Array<{
1075-
function: {
1076-
arguments: string;
1077-
name: string;
1078-
};
1079-
id: string;
1080-
type: string;
1081-
index: number;
1082-
}>;
1083-
};
1084-
}>;
1085-
created: number;
1086-
id: string;
1087-
usage: {
1088-
completion_tokens: number;
1089-
prompt_tokens: number;
1090-
prompt_tokens_details: {
1091-
cached_tokens: number;
1092-
};
1093-
total_tokens: number;
1094-
};
1095-
model: string;
1096-
object: string;
1097974
}

webpack.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async function getWebviewConfig(mode, env, entry) {
109109
rules: [
110110
{
111111
exclude: /node_modules/,
112-
include: [basePath, path.join(__dirname, 'src')],
112+
include: [basePath, path.join(__dirname, 'src'), path.join(__dirname, 'common')],
113113
test: /\.tsx?$/,
114114
use: env.esbuild
115115
? {
@@ -252,7 +252,7 @@ async function getExtensionConfig(target, mode, env) {
252252
rules: [
253253
{
254254
exclude: /node_modules/,
255-
include: path.join(__dirname, 'src'),
255+
include: [path.join(__dirname, 'src'), path.join(__dirname, 'common')],
256256
test: /\.tsx?$/,
257257
use: env.esbuild
258258
? {

webviews/sessionLogView/app.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import * as React from 'react';
99
import { createHighlighter } from 'shiki';
1010
import { vscode } from '../common/message';
1111
import type * as messages from './messages';
12-
import { parseSessionLogs, SessionInfo, SessionResponseLogChunk, SessionSetupStepResponse } from './sessionsApi';
12+
import { SessionInfo, SessionSetupStepResponse } from './sessionsApi';
1313
import { SessionView } from './sessionView';
14+
import { parseSessionLogs, SessionResponseLogChunk } from '../../common/sessionParsing';
1415

1516
const themeName = 'vscode-theme';
1617

webviews/sessionLogView/sessionView.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.main';
99
import * as React from 'react';
1010
import * as ReactDOM from 'react-dom';
1111
import { Temporal } from 'temporal-polyfill';
12+
import { parseToolCallDetails, SessionResponseLogChunk } from '../../common/sessionParsing';
1213
import { vscode } from '../common/message';
1314
import { CodeView } from './codeView';
1415
import './index.css'; // Create this file for styling
1516
import { PullInfo } from './messages';
16-
import { parseDiff, type SessionInfo, type SessionResponseLogChunk, type SessionSetupStepResponse } from './sessionsApi';
17+
import { parseDiff, type SessionInfo, type SessionSetupStepResponse } from './sessionsApi';
1718

1819
interface SessionViewProps {
1920
readonly pullInfo: PullInfo | undefined;
@@ -126,8 +127,9 @@ const SessionLog: React.FC<SessionLogProps> = ({ logs }) => {
126127
return;
127128
}
128129

129-
const args = JSON.parse(choice.delta.tool_calls[0].function.arguments);
130-
name = choice.delta.tool_calls[0].function.name;
130+
const toolCall = choice.delta.tool_calls[0];
131+
const args = JSON.parse(toolCall.function.arguments);
132+
name = toolCall.function.name;
131133

132134
if (name === 'str_replace_editor') {
133135
if (args.command === 'view') {

0 commit comments

Comments
 (0)