From c4f3fa4850af1703919991a7a81b80f6c5a0c90b Mon Sep 17 00:00:00 2001 From: Hai Phuc Nguyen <3423575+haiphucnguyen@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:14:27 -0700 Subject: [PATCH 1/2] Update --- .../core/providers/ChatRequestTransformers.kt | 18 ++++++++++++++++-- .../io/askimo/core/util/HttpClientUtils.kt | 19 ++++++++++++++----- .../core/util/LoggingHttpClientBuilder.kt | 14 ++++++++++++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/shared/src/main/kotlin/io/askimo/core/providers/ChatRequestTransformers.kt b/shared/src/main/kotlin/io/askimo/core/providers/ChatRequestTransformers.kt index ec98e60e6..34e171bb2 100644 --- a/shared/src/main/kotlin/io/askimo/core/providers/ChatRequestTransformers.kt +++ b/shared/src/main/kotlin/io/askimo/core/providers/ChatRequestTransformers.kt @@ -102,9 +102,23 @@ object ChatRequestTransformers { } } + // Remove consecutive duplicate non-system messages of the same type. + // Retries cause the same user message to be appended to memory on each attempt, + // producing back-to-back identical USER (or AI) messages. Drop any message whose + // type + text is identical to the immediately preceding message of the same type. + val deduplicatedNonSystem = nonSystemMessages.fold(mutableListOf()) { acc, msg -> + val lastSameType = acc.lastOrNull { it.type() == msg.type() } + if (lastSameType != null && getMessageText(lastSameType) == getMessageText(msg)) { + log.debug("Dropping consecutive duplicate {} message", msg.type()) + acc + } else { + acc.also { it.add(msg) } + } + } + // Preserve existing system messages (e.g. tool instructions from AiServiceBuilder), - // append new non-duplicate ones after, then all conversation messages - val rebuiltMessages = existingSystemMessages + additionalSystemMessages + nonSystemMessages + // append new non-duplicate ones after, then deduplicated conversation messages + val rebuiltMessages = existingSystemMessages + additionalSystemMessages + deduplicatedNonSystem return chatRequest.toBuilder().messages(rebuiltMessages).build() } diff --git a/shared/src/main/kotlin/io/askimo/core/util/HttpClientUtils.kt b/shared/src/main/kotlin/io/askimo/core/util/HttpClientUtils.kt index 42d7fb9f7..ac43235ee 100644 --- a/shared/src/main/kotlin/io/askimo/core/util/HttpClientUtils.kt +++ b/shared/src/main/kotlin/io/askimo/core/util/HttpClientUtils.kt @@ -18,6 +18,10 @@ import java.time.Duration * Proxy is automatically bypassed for localhost/private-IP URLs when [baseUrl] is provided. * Pass `null` for cloud providers (e.g. Anthropic, Gemini) where no local bypass is needed. * + * [builderTransform] is an optional lambda applied to the configured [HttpClient.Builder] + * before it is passed to [JdkHttpClient.builder]. Use it to wrap the builder in a decorator + * without duplicating proxy/timeout logic. + * * This is the shared HTTP-client factory used by all model factories — both those that extend * [io.askimo.core.providers.openaicompatible.OpenAiCompatibleChatModelFactory] and standalone factories * (Anthropic, Gemini) that implement [io.askimo.core.providers.ChatModelFactory] directly. @@ -25,13 +29,18 @@ import java.time.Duration fun createJdkHttpClientBuilder( baseUrl: String? = null, httpVersion: HttpVersion = HttpVersion.HTTP_2, -): JdkHttpClientBuilder = JdkHttpClient.builder().httpClientBuilder( - ProxyUtil.configureProxy( + builderTransform: ((HttpClient.Builder) -> HttpClient.Builder)? = null, +): JdkHttpClientBuilder { + val httpClientBuilder = ProxyUtil.configureProxy( HttpClient.newBuilder().version(httpVersion.toJdkVersion()), baseUrl, - ).withLoggingIfDebug(), -).readTimeout(Duration.ofSeconds(AppConfig.models.timeouts.defaultModelTimeoutSeconds)) - .connectTimeout(Duration.ofSeconds(AppConfig.models.timeouts.defaultModelTimeoutSeconds)) + ).withLoggingIfDebug() + val finalBuilder = builderTransform?.invoke(httpClientBuilder) ?: httpClientBuilder + return JdkHttpClient.builder() + .httpClientBuilder(finalBuilder) + .readTimeout(Duration.ofSeconds(AppConfig.models.timeouts.defaultModelTimeoutSeconds)) + .connectTimeout(Duration.ofSeconds(AppConfig.models.timeouts.defaultModelTimeoutSeconds)) +} /** * Maps the provider-agnostic [HttpVersion] enum to the JDK [HttpClient.Version] enum diff --git a/shared/src/main/kotlin/io/askimo/core/util/LoggingHttpClientBuilder.kt b/shared/src/main/kotlin/io/askimo/core/util/LoggingHttpClientBuilder.kt index d827693d0..467bb7f80 100644 --- a/shared/src/main/kotlin/io/askimo/core/util/LoggingHttpClientBuilder.kt +++ b/shared/src/main/kotlin/io/askimo/core/util/LoggingHttpClientBuilder.kt @@ -153,10 +153,14 @@ class LoggingHttpClient( sb.appendHeaders(responseInfo.headers()) if (bodyBytes.isNotEmpty()) { val text = bodyBytes.toString(Charsets.UTF_8) + val sizeLabel = if (truncated) "≥${MAX_RESPONSE_LOG_BYTES} bytes (truncated)" else "${bodyBytes.size} bytes" sb.appendLine() - sb.appendLine("Body${if (truncated) " [truncated at $MAX_RESPONSE_LOG_BYTES bytes]" else ""}:") + sb.appendLine("Body [$sizeLabel]:") sb.append(" ") sb.appendLine(text.replace("\n", "\n ")) + } else { + sb.appendLine() + sb.appendLine("Body [0 bytes]") } sb.append("───────────────────────────────────────────────────────────────────────") log.debug(sb.toString()) @@ -263,10 +267,16 @@ class LoggingHttpClient( sb.appendLine() sb.appendHeaders(request.headers()) if (body != null) { + val byteSize = body.toByteArray(Charsets.UTF_8).size + val truncated = body.endsWith("[truncated at 4 096 chars]") + val sizeLabel = if (truncated) "≥$byteSize bytes (truncated)" else "$byteSize bytes" sb.appendLine() - sb.appendLine("Body:") + sb.appendLine("Body [$sizeLabel]:") sb.append(" ") sb.appendLine(body.replace("\n", "\n ")) + } else { + sb.appendLine() + sb.appendLine("Body [0 bytes]") } sb.append("───────────────────────────────────────────────────────────────────────") log.debug(sb.toString()) From b656305c6fe422c7ba2093579da0f3b8f84d95f8 Mon Sep 17 00:00:00 2001 From: Hai Nguyen <3423575+haiphucnguyen@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:44:17 -0700 Subject: [PATCH 2/2] Update --- .../io/askimo/ui/chat/ChatInputField.kt | 35 +++++++++++-------- .../main/resources/i18n/messages.properties | 1 + .../resources/i18n/messages_de.properties | 1 + .../resources/i18n/messages_es.properties | 1 + .../resources/i18n/messages_fr.properties | 1 + .../resources/i18n/messages_ja_JP.properties | 1 + .../resources/i18n/messages_ko_KR.properties | 1 + .../resources/i18n/messages_pt_BR.properties | 1 + .../resources/i18n/messages_vi_VN.properties | 1 + .../resources/i18n/messages_zh_CN.properties | 1 + .../resources/i18n/messages_zh_TW.properties | 1 + .../repository/ChatDirectiveRepository.kt | 3 +- 12 files changed, 33 insertions(+), 15 deletions(-) diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt index 46c147a75..2bdfee1bd 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt @@ -776,20 +776,17 @@ fun chatInputField( ) // ── Directive chip — inline in controls row ───────────────── - // Shown whenever directives exist; styled to match toolsIndicatorButton. - if (availableDirectives.isNotEmpty()) { - Spacer(modifier = Modifier.width(6.dp)) - directiveChip( - availableDirectives = availableDirectives, - selectedDirective = selectedDirective, - isLoading = isLoading, - onToggleDirective = onToggleDirective, - directivePopupExpanded = directivePopupExpanded, - onDirectivePopupExpandedChange = { directivePopupExpanded = it }, - onShowNewDirectiveDialog = { showNewDirectiveDialog = true }, - onShowManageDirectivesDialog = { showManageDirectivesDialog = true }, - ) - } + Spacer(modifier = Modifier.width(6.dp)) + directiveChip( + availableDirectives = availableDirectives, + selectedDirective = selectedDirective, + isLoading = isLoading, + onToggleDirective = onToggleDirective, + directivePopupExpanded = directivePopupExpanded, + onDirectivePopupExpandedChange = { directivePopupExpanded = it }, + onShowNewDirectiveDialog = { showNewDirectiveDialog = true }, + onShowManageDirectivesDialog = { showManageDirectivesDialog = true }, + ) // ── Web search in RAG chip — only in project sessions when web search is configured ── if (isProjectSession && AppConfig.webSearch.enabled) { @@ -1843,6 +1840,16 @@ private fun directiveChip( .heightIn(max = 280.dp) .verticalScroll(rememberScrollState()), ) { + if (availableDirectives.isEmpty()) { + Text( + text = stringResource("chat.directive.empty"), + style = AppTextStyles.bodySecondary, + modifier = Modifier.padding( + horizontal = Spacing.medium, + vertical = Spacing.medium, + ), + ) + } availableDirectives.forEach { directive -> val isSelected = selectedDirective == directive.id themedRichTooltip( diff --git a/desktop-shared/src/main/resources/i18n/messages.properties b/desktop-shared/src/main/resources/i18n/messages.properties index b9871b7cc..f5e540f28 100644 --- a/desktop-shared/src/main/resources/i18n/messages.properties +++ b/desktop-shared/src/main/resources/i18n/messages.properties @@ -687,6 +687,7 @@ chat.directive=Directive chat.directive.new=New Directive chat.directive.manage=Manage Directives chat.directive.learn.more=Learn how to use directives +chat.directive.empty=No directives yet. Create one to steer the AI with custom instructions. chat.attach.file=Attach File ({0}+A) chat.attach.file.menu=Add Attachments chat.drop.files=Drop files to attach to the conversation diff --git a/desktop-shared/src/main/resources/i18n/messages_de.properties b/desktop-shared/src/main/resources/i18n/messages_de.properties index 0dc0475a3..a090b2f43 100644 --- a/desktop-shared/src/main/resources/i18n/messages_de.properties +++ b/desktop-shared/src/main/resources/i18n/messages_de.properties @@ -687,6 +687,7 @@ chat.directive=Direktive chat.directive.new=Neue Direktive chat.directive.manage=Direktiven verwalten chat.directive.learn.more=Erfahren Sie, wie Sie Direktiven verwenden +chat.directive.empty=Noch keine Direktiven. Erstellen Sie eine, um die KI mit benutzerdefinierten Anweisungen zu steuern. chat.attach.file=Datei anhängen ({0}+A) chat.attach.file.menu=Anhänge hinzufügen chat.drop.files=Dateien zum Anhängen an die Unterhaltung hierher ziehen diff --git a/desktop-shared/src/main/resources/i18n/messages_es.properties b/desktop-shared/src/main/resources/i18n/messages_es.properties index 3ff7bb268..5b6c91c87 100644 --- a/desktop-shared/src/main/resources/i18n/messages_es.properties +++ b/desktop-shared/src/main/resources/i18n/messages_es.properties @@ -687,6 +687,7 @@ chat.directive=Directiva: chat.directive.new=Nueva directiva chat.directive.manage=Gestionar directivas chat.directive.learn.more=Aprenda a usar directivas +chat.directive.empty=Aún no hay directivas. Crea una para guiar la IA con instrucciones personalizadas. chat.attach.file=Adjuntar archivo ({0}+A) chat.attach.file.menu=Agregar archivos adjuntos chat.drop.files=Arrastra archivos para adjuntarlos a la conversación diff --git a/desktop-shared/src/main/resources/i18n/messages_fr.properties b/desktop-shared/src/main/resources/i18n/messages_fr.properties index 2136471f1..e6bc2f88a 100644 --- a/desktop-shared/src/main/resources/i18n/messages_fr.properties +++ b/desktop-shared/src/main/resources/i18n/messages_fr.properties @@ -687,6 +687,7 @@ chat.directive=Directive chat.directive.new=Nouvelle directive chat.directive.manage=Gérer les directives chat.directive.learn.more=Apprenez à utiliser les directives +chat.directive.empty=Aucune directive pour l'instant. Créez-en une pour guider l'IA avec des instructions personnalisées. chat.attach.file=Joindre un fichier ({0}+A) chat.attach.file.menu=Ajouter des pièces jointes chat.drop.files=Glissez-déposez des fichiers pour les joindre à la conversation diff --git a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties index 5d30cb7ce..faa8ef02d 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties @@ -687,6 +687,7 @@ chat.directive=ディレクティブ chat.directive.new=新しいディレクティブ chat.directive.manage=ディレクティブ管理 chat.directive.learn.more=ディレクティブの使い方を学ぶ +chat.directive.empty=ディレクティブがまだありません。カスタム指示でAIを誘導するものを作成してください。 chat.attach.file=ファイル添付 ({0}+A) chat.attach.file.menu=添付ファイルを追加 chat.drop.files=ファイルをドラッグ&ドロップして会話に添付 diff --git a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties index 45bc9b584..d219c6b8f 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties @@ -687,6 +687,7 @@ chat.directive=지시문 chat.directive.new=새 지시문 chat.directive.manage=지시문 관리 chat.directive.learn.more=디렉티브 사용법을 배워보세요 +chat.directive.empty=아직 지시문이 없습니다. 하나를 만들어 AI에게 사용자 지정 지침을 제공하세요. chat.attach.file=파일 첨부 ({0}+A) chat.attach.file.menu=첨부 파일 추가 chat.drop.files=파일을 드래그하여 대화에 첨부하세요 diff --git a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties index 8d6627da9..b49241ddb 100644 --- a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties @@ -687,6 +687,7 @@ chat.directive=Diretiva chat.directive.new=Nova Diretiva chat.directive.manage=Gerenciar Diretivas chat.directive.learn.more=Aprenda a usar diretivas +chat.directive.empty=Nenhuma diretiva ainda. Crie uma para guiar a IA com instruções personalizadas. chat.attach.file=Anexar Arquivo ({0}+A) chat.attach.file.menu=Adicionar Anexos chat.drop.files=Arraste arquivos para anexar à conversa diff --git a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties index f27466f2f..194067a3f 100644 --- a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties @@ -687,6 +687,7 @@ chat.directive=Chỉ thị chat.directive.new=Chỉ thị mới chat.directive.manage=Quản lý chỉ thị chat.directive.learn.more=Tìm hiểu cách sử dụng chỉ thị +chat.directive.empty=Chưa có chỉ thị nào. Tạo một chỉ thị để hướng dẫn AI với các hướng dẫn tùy chỉnh. chat.attach.file=Đính kèm tệp ({0}+A) chat.attach.file.menu=Thêm tệp đính kèm chat.drop.files=Kéo thả tệp để đính kèm vào cuộc trò chuyện diff --git a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties index d39563060..afe97e337 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties @@ -687,6 +687,7 @@ chat.directive=指令 chat.directive.new=新指令 chat.directive.manage=管理指令 chat.directive.learn.more=了解如何使用指令 +chat.directive.empty=暂无指令。创建一个以使用自定义说明引导 AI。 chat.attach.file=附加文件 ({0}+A) chat.attach.file.menu=添加附件 chat.drop.files=拖拽文件以将其附加到对话中 diff --git a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties index 438437d53..42c8bb975 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties @@ -687,6 +687,7 @@ chat.directive=指令 chat.directive.new=新增指令 chat.directive.manage=管理指令 chat.directive.learn.more=了解如何使用指令 +chat.directive.empty=尚無指令。建立一個以使用自訂說明引導 AI。 chat.attach.file=附加檔案 ({0}+A) chat.attach.file.menu=新增附件 chat.drop.files=拖曳檔案以將其附加到對話中 diff --git a/shared/src/main/kotlin/io/askimo/core/chat/repository/ChatDirectiveRepository.kt b/shared/src/main/kotlin/io/askimo/core/chat/repository/ChatDirectiveRepository.kt index 0e1d1b547..d67859e30 100644 --- a/shared/src/main/kotlin/io/askimo/core/chat/repository/ChatDirectiveRepository.kt +++ b/shared/src/main/kotlin/io/askimo/core/chat/repository/ChatDirectiveRepository.kt @@ -72,6 +72,8 @@ class ChatDirectiveRepository internal constructor( databaseManager: DatabaseManager = DatabaseManager.getInstance(), ) : AbstractSQLiteRepository(databaseManager) { + val log = logger() + /** * Save a new directive or update existing one. * @throws IllegalArgumentException if name or content exceed max length @@ -279,7 +281,6 @@ class ChatDirectiveRepository internal constructor( * into the local database. */ fun seedDefaultDirectives() { - val log = logger() val resourceUrl = ChatDirectiveRepository::class.java.getResource("/directives/") if (resourceUrl == null) { log.debug("No /directives/ resource directory found on classpath — skipping seed")