Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions desktop-shared/src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=ファイルをドラッグ&ドロップして会話に添付
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=파일을 드래그하여 대화에 첨부하세요
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=拖拽文件以将其附加到对话中
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=拖曳檔案以將其附加到對話中
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class ChatDirectiveRepository internal constructor(
databaseManager: DatabaseManager = DatabaseManager.getInstance(),
) : AbstractSQLiteRepository(databaseManager) {

val log = logger<ChatDirectiveRepository>()

/**
* Save a new directive or update existing one.
* @throws IllegalArgumentException if name or content exceed max length
Expand Down Expand Up @@ -279,7 +281,6 @@ class ChatDirectiveRepository internal constructor(
* into the local database.
*/
fun seedDefaultDirectives() {
val log = logger<ChatDirectiveRepository>()
val resourceUrl = ChatDirectiveRepository::class.java.getResource("/directives/")
if (resourceUrl == null) {
log.debug("No /directives/ resource directory found on classpath — skipping seed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatMessage>()) { 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()
}

Expand Down
19 changes: 14 additions & 5 deletions shared/src/main/kotlin/io/askimo/core/util/HttpClientUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,29 @@ 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.
*/
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down