feat: psi structure for chat (#897)

* Change SwingDispatcher to Dispatchers.Main.immediate

* Change PSI analyzer package

* Added a blank for PSI analysis in the chat

* Add support for code structure analysis and improve tag management

- Refactor TagManager to use thread-safe collections
- Add support for new tag types (EditorTagDetails, FileTagDetails)
- Update UI components to handle structure analysis
- Add new icon for structure tags

* Refactoring tags v2

* Add PSI structure to chat settings

* Add VirtualFile to ClassStructure and improve PSI token tracking

* Support passing PSI structure to completion requests

* Add removeListener method to TagManager and fix memory leak

* Update buildOpenAIMessages to support PSI structure for all providers

* Add selected editor tag when initializing user input header

* Add chat settings configuration screen

* Remove unused editor tag and PSI structure settings panels

---------

Co-authored-by: a.iudin <a.iudin@vk.team>
This commit is contained in:
Aleksandr 2025-03-11 20:57:55 +03:00 committed by GitHub
parent 97185109ad
commit 7d472e8cf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1185 additions and 444 deletions

View file

@ -25,6 +25,7 @@ public final class Icons {
public static final Icon You = IconLoader.getIcon("/icons/you.svg", Icons.class);
public static final Icon Ollama = IconLoader.getIcon("/icons/ollama.svg", Icons.class);
public static final Icon User = IconLoader.getIcon("/icons/user.svg", Icons.class);
public static final Icon Tree = IconLoader.getIcon("/icons/tree.svg", Icons.class);
public static final Icon Lightning = IconLoader.getIcon("/icons/lightning.svg", Icons.class);
public static final Icon LightningDisabled =
IconLoader.getIcon("/icons/lightning.svg", Icons.class);

View file

@ -20,6 +20,7 @@ import ee.carlrobert.codegpt.ui.OverlayUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.jetbrains.annotations.NotNull;
@ -69,7 +70,8 @@ public class ProjectCompilationStatusListener implements CompilationStatusListen
.toList());
message.setPrompt(CompletionRequestUtil.getPromptWithContext(
new ArrayList<>(errorMapping.keySet()),
prompt));
prompt,
new HashSet<>()));
return message;
}

View file

@ -21,10 +21,14 @@ import ee.carlrobert.codegpt.completions.ToolwindowChatCompletionRequestHandler;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.psistructure.PsiStructureProvider;
import ee.carlrobert.codegpt.psistructure.models.ClassStructure;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.telemetry.TelemetryAction;
import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction;
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository;
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureState;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails;
@ -34,17 +38,24 @@ import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel;
import ee.carlrobert.codegpt.toolwindow.ui.UserMessagePanel;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
import ee.carlrobert.codegpt.ui.textarea.header.tag.EditorTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.GitCommitTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.PersonaTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager;
import ee.carlrobert.codegpt.util.EditorUtil;
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers;
import ee.carlrobert.codegpt.util.file.FileUtil;
import git4idea.GitCommit;
import java.awt.BorderLayout;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.swing.JComponent;
import javax.swing.JPanel;
import kotlin.Unit;
@ -64,6 +75,8 @@ public class ChatToolWindowTabPanel implements Disposable {
private final ConversationService conversationService;
private final TotalTokensPanel totalTokensPanel;
private final ChatToolWindowScrollablePanel toolWindowScrollablePanel;
private final PsiStructureRepository psiStructureRepository;
private final TagManager tagManager;
private @Nullable ToolwindowChatCompletionRequestHandler requestHandler;
@ -73,16 +86,27 @@ public class ChatToolWindowTabPanel implements Disposable {
this.chatSession = new ChatSession();
conversationService = ConversationService.getInstance();
toolWindowScrollablePanel = new ChatToolWindowScrollablePanel();
tagManager = new TagManager(this);
this.psiStructureRepository = new PsiStructureRepository(
this,
project,
tagManager,
new PsiStructureProvider(),
new CoroutineDispatchers()
);
totalTokensPanel = new TotalTokensPanel(
project,
conversation,
EditorUtil.getSelectedEditorSelectedText(project),
this);
this,
psiStructureRepository);
userInputPanel = new UserInputPanel(
project,
conversation,
totalTokensPanel,
this,
tagManager,
this::handleSubmit,
this::handleCancel);
userInputPanel.requestFocus();
@ -134,13 +158,19 @@ public class ChatToolWindowTabPanel implements Disposable {
private ChatCompletionParameters getCallParameters(
Message message,
ConversationType conversationType) {
var selectedTags = userInputPanel.getSelectedTags();
ConversationType conversationType,
Set<ClassStructure> psiStructure
) {
final var selectedTags = tagManager.getTags().stream()
.filter(TagDetails::getSelected)
.collect(Collectors.toList());
var builder = ChatCompletionParameters.builder(conversation, message)
.sessionId(chatSession.getId())
.conversationType(conversationType)
.imageDetailsFromPath(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project))
.referencedFiles(getReferencedFiles(selectedTags));
.referencedFiles(getReferencedFiles(selectedTags))
.psiStructure(psiStructure);
findTagOfType(selectedTags, PersonaTagDetails.class)
.ifPresent(tag -> builder.personaDetails(tag.getPersonaDetails()));
@ -151,17 +181,27 @@ public class ChatToolWindowTabPanel implements Disposable {
return builder.build();
}
private List<ReferencedFile> getReferencedFiles() {
return getReferencedFiles(userInputPanel.getSelectedTags());
}
private List<ReferencedFile> getReferencedFiles(List<? extends TagDetails> tags) {
return tags.stream()
.filter(FileTagDetails.class::isInstance)
.map(it -> ReferencedFile.from(((FileTagDetails) it).getVirtualFile()))
.map(this::getVirtualFile)
.filter(Objects::nonNull)
.map(ReferencedFile::from)
.toList();
}
private VirtualFile getVirtualFile(TagDetails tag) {
VirtualFile virtualFile = null;
if (tag.getSelected()) {
if (tag instanceof FileTagDetails) {
virtualFile = ((FileTagDetails) tag).getVirtualFile();
} else if (tag instanceof EditorTagDetails) {
virtualFile = ((EditorTagDetails) tag).getVirtualFile();
}
}
return virtualFile;
}
private <T extends TagDetails> Optional<T> findTagOfType(
List<? extends TagDetails> tags,
Class<T> tagClass) {
@ -172,8 +212,16 @@ public class ChatToolWindowTabPanel implements Disposable {
}
public void sendMessage(Message message, ConversationType conversationType) {
sendMessage(message, conversationType, new HashSet<>());
}
public void sendMessage(
Message message,
ConversationType conversationType,
Set<ClassStructure> psiStructure
) {
ApplicationManager.getApplication().invokeLater(() -> {
var callParameters = getCallParameters(message, conversationType);
var callParameters = getCallParameters(message, conversationType, psiStructure);
if (callParameters.getImageDetails() != null) {
project.getService(ChatToolWindowContentManager.class)
.tryFindChatToolWindowPanel()
@ -233,7 +281,7 @@ public class ChatToolWindowTabPanel implements Disposable {
}
private void reloadMessage(ChatCompletionParameters prevParameters,
UserMessagePanel userMessagePanel) {
UserMessagePanel userMessagePanel) {
var prevMessage = prevParameters.getMessage();
ResponseMessagePanel responsePanel = null;
try {
@ -307,10 +355,22 @@ public class ChatToolWindowTabPanel implements Disposable {
.executeOnPooledThread(() -> requestHandler.call(callParameters));
}
private Unit handleSubmit(String text, List<? extends TagDetails> appliedTags) {
private Unit handleSubmit(String text) {
final Set<ClassStructure> psiStructure;
if (psiStructureRepository.getStructureState().getValue()
instanceof PsiStructureState.Content content) {
psiStructure = content.getElements();
} else {
psiStructure = new HashSet<>();
}
final var appliedTags = tagManager.getTags().stream()
.filter(TagDetails::getSelected)
.collect(Collectors.toList());
var messageBuilder = new MessageBuilder(project, text).withInlays(appliedTags);
List<ReferencedFile> referencedFiles = getReferencedFiles();
List<ReferencedFile> referencedFiles = getReferencedFiles(appliedTags);
if (!referencedFiles.isEmpty()) {
messageBuilder.withReferencedFiles(referencedFiles);
}
@ -320,7 +380,7 @@ public class ChatToolWindowTabPanel implements Disposable {
messageBuilder.withImage(attachedImagePath);
}
sendMessage(messageBuilder.build(), ConversationType.DEFAULT);
sendMessage(messageBuilder.build(), ConversationType.DEFAULT, psiStructure);
return Unit.INSTANCE;
}
@ -336,6 +396,7 @@ public class ChatToolWindowTabPanel implements Disposable {
panel.setBorder(JBUI.Borders.compound(
JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0),
JBUI.Borders.empty(8)));
if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT) {
panel.add(JBUI.Panels.simplePanel(totalTokensPanel)
.withBorder(JBUI.Borders.emptyBottom(8)), BorderLayout.NORTH);

View file

@ -0,0 +1,251 @@
package ee.carlrobert.codegpt.toolwindow.chat.structure.data
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.AsyncFileListener
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.util.io.await
import ee.carlrobert.codegpt.psistructure.PsiStructureProvider
import ee.carlrobert.codegpt.settings.chat.ChatSettingsListener
import ee.carlrobert.codegpt.ui.textarea.header.tag.CurrentGitChangesTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.DocumentationTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.EditorSelectionTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.EditorTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.EmptyTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.FolderTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.GitCommitTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.PersonaTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.SelectionTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManagerListener
import ee.carlrobert.codegpt.ui.textarea.header.tag.WebTagDetails
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers
import ee.carlrobert.codegpt.util.coroutines.DisposableCoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class PsiStructureRepository(
parentDisposable: Disposable,
private val project: Project,
private val tagManager: TagManager,
private val psiStructureProvider: PsiStructureProvider,
private val dispatchers: CoroutineDispatchers,
) {
private val mutex = Mutex()
private val coroutineScope = DisposableCoroutineScope(dispatchers.io())
@Volatile
private var updatePsiStructureJob: Job? = null
private val tagsListener = object : TagManagerListener {
override fun onTagAdded(tag: TagDetails) {
updatePsiStructureIfNeeded()
}
override fun onTagRemoved(tag: TagDetails) {
updatePsiStructureIfNeeded()
}
override fun onTagSelectionChanged(tag: TagDetails) {
val tags = tagManager.getTags().getPsiAnalyzedTags()
update(tags)
}
private fun updatePsiStructureIfNeeded() {
val tags = tagManager.getTags().getPsiAnalyzedTags()
if (isNeedUpdatePsiStructure(tags)) {
update(tags)
}
}
private fun isNeedUpdatePsiStructure(tagsForAnalyze: Set<TagDetails>): Boolean {
val currentlyAnalyzedTags = when (val currentState = _structureState.value) {
is PsiStructureState.Content -> currentState.currentlyAnalyzedTags
is PsiStructureState.UpdateInProgress -> currentState.currentlyAnalyzedTags
PsiStructureState.Disabled -> emptySet()
}
return tagsForAnalyze.toVirtualFilesSet() != currentlyAnalyzedTags.toVirtualFilesSet()
}
}
private val asyncFileListener = AsyncFileListener { events ->
val currentlyAnalyzedTags = when (val currentState = _structureState.value) {
is PsiStructureState.Content -> currentState.currentlyAnalyzedTags
is PsiStructureState.UpdateInProgress -> currentState.currentlyAnalyzedTags
PsiStructureState.Disabled -> emptySet()
}
val currentlyAnalyzedFiles = currentlyAnalyzedTags.toVirtualFilesSet()
val hasRelevantChanges = events.any { event ->
event.file?.let { it in currentlyAnalyzedFiles } == true
}
if (hasRelevantChanges) {
object : AsyncFileListener.ChangeApplier {
override fun afterVfsChange() {
update(currentlyAnalyzedTags)
}
}
} else {
null
}
}
init {
Disposer.register(parentDisposable, coroutineScope)
tagManager.addListener(tagsListener)
Disposer.register(parentDisposable) {
tagManager.removeListener(tagsListener)
}
VirtualFileManager.getInstance().addAsyncFileListener(asyncFileListener, parentDisposable)
val connection = ApplicationManager.getApplication().messageBus
.connect(parentDisposable)
connection.subscribe(
ChatSettingsListener.TOPIC,
ChatSettingsListener { newState ->
if (newState.psiStructureEnabled) {
enable()
} else {
disable()
}
})
}
private val _structureState: MutableStateFlow<PsiStructureState> = MutableStateFlow(
PsiStructureState.Content(emptySet(), emptySet())
)
val structureState = _structureState.asStateFlow()
private fun disable() {
updatePsiStructureJob?.cancel()
_structureState.value = PsiStructureState.Disabled
}
private fun enable() {
val tags = tagManager.getTags().getPsiAnalyzedTags()
_structureState.value = PsiStructureState.UpdateInProgress(tags)
update(tags)
}
private fun update(tags: Set<TagDetails>) {
updatePsiStructureJob?.cancel()
updatePsiStructureJob = coroutineScope.launch {
mutex.withLock {
if (_structureState.value == PsiStructureState.Disabled) return@launch
_structureState.value = PsiStructureState.UpdateInProgress(tags)
val tagsVirtualFiles = tags.toVirtualFilesSet()
val coroutineContext = currentCoroutineContext()
val psiFiles = ReadAction.nonBlocking<List<PsiFile>> {
tagsVirtualFiles
.mapNotNull { virtualFile ->
coroutineContext.ensureActive()
PsiManager.getInstance(project).findFile(virtualFile)
}
}
.inSmartMode(project)
.submit(dispatchers.default().asExecutor())
.await()
val virtualFilesToRemoveFromStructure = tags.getExcludedVirtualFiles()
val result = psiStructureProvider.get(psiFiles)
.filter { classStructure ->
!virtualFilesToRemoveFromStructure.contains(classStructure.virtualFile)
}
.toSet()
_structureState.value = PsiStructureState.Content(tags, result)
}
}
}
private fun Set<TagDetails>.getExcludedVirtualFiles(): Set<VirtualFile> =
mapNotNull { tagDetails ->
if (!tagDetails.selected) {
null
} else {
when (tagDetails) {
is SelectionTagDetails -> tagDetails.virtualFile
is FileTagDetails -> tagDetails.virtualFile
is EditorTagDetails -> tagDetails.virtualFile
// Maybe need recursive find all files
is FolderTagDetails -> null
is EditorSelectionTagDetails -> null
is DocumentationTagDetails -> null
is CurrentGitChangesTagDetails -> null
is GitCommitTagDetails -> null
is PersonaTagDetails -> null
is EmptyTagDetails -> null
is WebTagDetails -> null
}
}
}
.toSet()
private fun Set<TagDetails>.getPsiAnalyzedTags(): Set<TagDetails> =
filter { tagDetails ->
when (tagDetails) {
is SelectionTagDetails -> tagDetails.selected
is FileTagDetails -> tagDetails.selected
is EditorSelectionTagDetails -> tagDetails.selected
is EditorTagDetails -> tagDetails.selected
// Maybe need recursive find all files
is FolderTagDetails -> false
is DocumentationTagDetails -> false
is CurrentGitChangesTagDetails -> false
is GitCommitTagDetails -> false
is PersonaTagDetails -> false
is EmptyTagDetails -> false
is WebTagDetails -> false
}
}
.toSet()
private fun Set<TagDetails>.toVirtualFilesSet(): Set<VirtualFile> =
mapNotNull { tagDetails ->
if (!tagDetails.selected) {
null
} else {
when (tagDetails) {
is SelectionTagDetails -> tagDetails.virtualFile
is FileTagDetails -> tagDetails.virtualFile
is EditorSelectionTagDetails -> tagDetails.virtualFile
is EditorTagDetails -> tagDetails.virtualFile
// Maybe need recursive find all files
is FolderTagDetails -> null
is DocumentationTagDetails -> null
is CurrentGitChangesTagDetails -> null
is GitCommitTagDetails -> null
is PersonaTagDetails -> null
is EmptyTagDetails -> null
is WebTagDetails -> null
}
}
}
.toSet()
}

View file

@ -0,0 +1,18 @@
package ee.carlrobert.codegpt.toolwindow.chat.structure.data
import ee.carlrobert.codegpt.psistructure.models.ClassStructure
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
sealed class PsiStructureState {
data class UpdateInProgress(
val currentlyAnalyzedTags: Set<TagDetails>,
) : PsiStructureState()
data object Disabled : PsiStructureState()
data class Content(
val currentlyAnalyzedTags: Set<TagDetails>,
val elements: Set<ClassStructure>
) : PsiStructureState()
}

View file

@ -0,0 +1,57 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import ee.carlrobert.codegpt.EncodingManager
import ee.carlrobert.codegpt.psistructure.ClassStructureSerializer
import ee.carlrobert.codegpt.psistructure.models.ClassStructure
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureState
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers
import ee.carlrobert.codegpt.util.coroutines.DisposableCoroutineScope
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
class PsiStructureTotalTokenProvider(
parentDisposable: Disposable,
private val classStructureSerializer: ClassStructureSerializer,
private val encodingManager: EncodingManager,
dispatchers: CoroutineDispatchers,
psiStructureRepository: PsiStructureRepository,
onPsiTokenHandled: (Int) -> Unit
) {
private val coroutineScope = DisposableCoroutineScope()
init {
Disposer.register(parentDisposable, coroutineScope)
psiStructureRepository.structureState
.map { structureState ->
when (structureState) {
is PsiStructureState.Content -> {
getPsiTokensCount(structureState.elements)
}
PsiStructureState.Disabled -> 0
is PsiStructureState.UpdateInProgress -> 0
}
}
.flowOn(dispatchers.io())
.onEach { psiTokens ->
onPsiTokenHandled(psiTokens)
}
.launchIn(coroutineScope)
}
private fun getPsiTokensCount(psiStructureSet: Set<ClassStructure>): Int =
psiStructureSet
.joinToString(separator = "\n\n") { psiStructure ->
classStructureSerializer.serialize(psiStructure)
}
.let { serializedPsiStructure ->
encodingManager.countTokens(serializedPsiStructure)
}
}

View file

@ -7,6 +7,7 @@ public class TotalTokensDetails {
private int userPromptTokens;
private int highlightedTokens;
private int referencedFilesTokens;
private int psiTokens;
public TotalTokensDetails(int systemPromptTokens) {
this.systemPromptTokens = systemPromptTokens;
@ -20,6 +21,10 @@ public class TotalTokensDetails {
this.conversationTokens = conversationTokens;
}
public void setPsiTokens(int psiTokens) {
this.psiTokens = psiTokens;
}
public int getConversationTokens() {
return conversationTokens;
}
@ -48,11 +53,16 @@ public class TotalTokensDetails {
return referencedFilesTokens;
}
public int getPsiTokens() {
return psiTokens;
}
public int getTotal() {
return systemPromptTokens
+ conversationTokens
+ userPromptTokens
+ highlightedTokens
+ referencedFilesTokens;
+ referencedFilesTokens
+ psiTokens;
}
}

View file

@ -18,9 +18,12 @@ import ee.carlrobert.codegpt.EncodingManager;
import ee.carlrobert.codegpt.ReferencedFile;
import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.psistructure.ClassStructureSerializer;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository;
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers;
import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
@ -30,6 +33,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import javax.swing.Box;
import javax.swing.JPanel;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -43,11 +47,25 @@ public class TotalTokensPanel extends JPanel {
@NotNull Project project,
Conversation conversation,
@Nullable String highlightedText,
Disposable parentDisposable) {
Disposable parentDisposable,
PsiStructureRepository psiStructureRepository
) {
super(new FlowLayout(FlowLayout.LEADING, 0, 0));
this.totalTokensDetails = createTokenDetails(conversation, highlightedText);
this.label = getLabel(totalTokensDetails);
new PsiStructureTotalTokenProvider(
parentDisposable,
ClassStructureSerializer.INSTANCE,
encodingManager,
new CoroutineDispatchers(),
psiStructureRepository,
psiTokens -> {
updatePsiTokenCount(psiTokens);
return Unit.INSTANCE;
}
);
setBorder(JBUI.Borders.empty(4));
setOpaque(false);
add(getContextHelpIcon(totalTokensDetails));
@ -104,6 +122,11 @@ public class TotalTokensPanel extends JPanel {
label.setText(getLabelHtml(total));
}
public void updatePsiTokenCount(int psiTokenCount) {
totalTokensDetails.setPsiTokens(psiTokenCount);
update();
}
public void updateConversationTokens(Conversation conversation) {
totalTokensDetails.setConversationTokens(encodingManager.countConversationTokens(conversation));
update();
@ -148,7 +171,8 @@ public class TotalTokensPanel extends JPanel {
"Conversation Tokens", totalTokensDetails.getConversationTokens(),
"Input Tokens", totalTokensDetails.getUserPromptTokens(),
"Highlighted Tokens", totalTokensDetails.getHighlightedTokens(),
"Referenced Files Tokens", totalTokensDetails.getReferencedFilesTokens()))
"Referenced Files Tokens", totalTokensDetails.getReferencedFilesTokens(),
"Dependency structure Tokens", totalTokensDetails.getPsiTokens()))
.entrySet().stream()
.map(entry -> format(
"<p style=\"margin: 0; padding: 0;\"><small>%s: <strong>%d</strong></small></p>",