From cfb132e70484743bd6b36d78624c78824e83eba1 Mon Sep 17 00:00:00 2001 From: larry-ca <199170495+larry-ca@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:52:18 -0700 Subject: [PATCH] Move the directive dropdown from the chat header to the chat input field --- .../io/askimo/ui/chat/ChatInputField.kt | 393 +++++++++++++++++- .../main/kotlin/io/askimo/ui/chat/ChatView.kt | 323 ++------------ .../main/resources/i18n/messages.properties | 3 +- .../resources/i18n/messages_de.properties | 3 +- .../resources/i18n/messages_es.properties | 1 - .../resources/i18n/messages_fr.properties | 3 +- .../resources/i18n/messages_ja_JP.properties | 3 +- .../resources/i18n/messages_ko_KR.properties | 3 +- .../resources/i18n/messages_pt_BR.properties | 3 +- .../resources/i18n/messages_vi_VN.properties | 3 +- .../resources/i18n/messages_zh_CN.properties | 3 +- .../resources/i18n/messages_zh_TW.properties | 3 +- 12 files changed, 426 insertions(+), 318 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 ecd2ff66..c58f2c69 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 @@ -23,13 +23,17 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Close @@ -74,6 +78,7 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -81,11 +86,21 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import io.askimo.core.AppConstants.DOMAIN +import io.askimo.core.chat.domain.ChatDirective +import io.askimo.core.chat.domain.DirectiveScope import io.askimo.core.chat.dto.ChatMessageDTO import io.askimo.core.chat.dto.FileAttachmentDTO +import io.askimo.core.chat.service.DirectiveImportResult import io.askimo.core.chat.util.FileContentExtractor import io.askimo.core.config.AppConfig import io.askimo.core.context.AppContext @@ -111,8 +126,12 @@ import io.askimo.ui.common.theme.AppComponents import io.askimo.ui.common.theme.AppComponents.dropdownMenu import io.askimo.ui.common.theme.LocalFontScale import io.askimo.ui.common.theme.Spacing +import io.askimo.ui.common.ui.TooltipPlacement +import io.askimo.ui.common.ui.themedRichTooltip import io.askimo.ui.common.ui.themedTooltip import io.askimo.ui.common.ui.util.FileDialogUtils +import io.askimo.ui.session.manageDirectivesDialog +import io.askimo.ui.session.newDirectiveDialog import io.askimo.ui.util.Platform import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -166,6 +185,15 @@ fun chatInputField( placeholder: String = stringResource("chat.input.placeholder"), onEnabledServerIdsChange: ((Set) -> Unit)? = null, onNavigateToMcpSettings: (() -> Unit)? = null, + // Directives chip + availableDirectives: List = emptyList(), + selectedDirective: String? = null, + onToggleDirective: (String?) -> Unit = {}, + onDirectiveCreated: ((name: String, content: String, applyToCurrent: Boolean) -> Unit)? = null, + onDirectiveUpdated: ((id: String, newName: String, newContent: String) -> Unit)? = null, + onDirectiveDeleted: ((id: String) -> Unit)? = null, + onDirectiveExported: (() -> String)? = null, + onDirectiveImported: ((json: String) -> DirectiveImportResult)? = null, modifier: Modifier = Modifier, ) { val inputFocusRequester = remember { FocusRequester() } @@ -295,6 +323,11 @@ fun chatInputField( onEnabledServerIdsChange?.invoke(enabledServerIds) } + // Directives chip popup / dialog state + var directivePopupExpanded by remember { mutableStateOf(false) } + var showNewDirectiveDialog by remember { mutableStateOf(false) } + var showManageDirectivesDialog by remember { mutableStateOf(false) } + // State for resizable text field. val fontScale = LocalFontScale.current val inlineControlsBottomPadding = 44.dp @@ -651,7 +684,7 @@ fun chatInputField( ) // ── Controls row ─────────────────────────────────────────────── - // Layout: [attach | image | tools | image-mode-chip] [reasoning chip] [send/stop] + // Layout: [attach | image | tools | directive | image-mode-chip] [reasoning chip] [send/stop] Row( modifier = Modifier .fillMaxWidth() @@ -743,6 +776,332 @@ fun chatInputField( modelSupportsTools = modelSupportsTools, ) + // ── Directive chip — inline in controls row ───────────────── + // Shown whenever directives exist; styled to match toolsIndicatorButton. + if (availableDirectives.isNotEmpty()) { + Spacer(modifier = Modifier.width(2.dp)) + Box { + val activeDirective = availableDirectives + .find { it.id == selectedDirective } + val activeDirectiveName = activeDirective?.name + + val chipAnchor: @Composable (@Composable () -> Unit) -> Unit = + if (activeDirective != null) { + { content -> + themedRichTooltip( + tooltipContent = { + Column( + modifier = Modifier + .widthIn(min = 400.dp, max = 500.dp) + .padding( + horizontal = Spacing.medium, + vertical = Spacing.small, + ), + verticalArrangement = Arrangement.spacedBy(Spacing.small), + ) { + Text( + text = activeDirective.name, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy( + alpha = 0.5f, + ), + ) + Text( + text = activeDirective.content, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 10, + overflow = TextOverflow.Ellipsis, + ) + } + }, + content = content, + ) + } + } else { + { content -> + themedTooltip( + text = stringResource("chat.directive"), + content = content, + ) + } + } + + chipAnchor { + Surface( + shape = RoundedCornerShape(8.dp), + color = if (selectedDirective != null) { + MaterialTheme.colorScheme.secondaryContainer + } else { + Color.Transparent + }, + tonalElevation = if (selectedDirective != null) 2.dp else 0.dp, + modifier = Modifier + .height(28.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable( + enabled = !isLoading, + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { directivePopupExpanded = true }, + ) + .pointerHoverIcon(PointerIcon.Hand), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 8.dp), + ) { + Icon( + Icons.Default.AutoAwesome, + contentDescription = stringResource("chat.directive"), + tint = if (selectedDirective != null) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.onSurface + }, + modifier = Modifier.size(16.dp), + ) + Text( + text = activeDirectiveName ?: stringResource("chat.directive"), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.SemiBold, + color = if (selectedDirective != null) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.onSurface + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 80.dp), + ) + } + } + } + + // Upward-opening popup anchored to the chip's top edge + if (directivePopupExpanded) { + Popup( + popupPositionProvider = object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset = IntOffset( + x = anchorBounds.left, + y = anchorBounds.top - popupContentSize.height - 4, + ) + }, + onDismissRequest = { directivePopupExpanded = false }, + properties = PopupProperties(focusable = true), + ) { + Surface( + modifier = Modifier + .widthIn(min = 350.dp, max = 420.dp), + shape = RoundedCornerShape(8.dp), + shadowElevation = 8.dp, + tonalElevation = 2.dp, + color = MaterialTheme.colorScheme.surface, + ) { + Column { + Text( + text = stringResource("chat.directive"), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding( + horizontal = Spacing.medium, + vertical = Spacing.small, + ), + ) + HorizontalDivider() + + // Directive rows — scrollable list, footer items always visible below + LazyColumn(modifier = Modifier.heightIn(max = 192.dp)) { + items(availableDirectives) { directive -> + val isSelected = selectedDirective == directive.id + themedRichTooltip( + placement = TooltipPlacement.RIGHT, + tooltipContent = { + Column( + modifier = Modifier + .widthIn(min = 350.dp, max = 420.dp) + .padding( + horizontal = Spacing.medium, + vertical = Spacing.small, + ), + verticalArrangement = Arrangement.spacedBy(Spacing.small), + ) { + Text( + text = directive.name, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy( + alpha = 0.5f, + ), + ) + Text( + text = directive.content, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 10, + overflow = TextOverflow.Ellipsis, + ) + } + }, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onToggleDirective( + if (isSelected) null else directive.id, + ) + } + .padding( + start = Spacing.extraSmall, + end = Spacing.small, + top = 2.dp, + bottom = 2.dp, + ) + .pointerHoverIcon(PointerIcon.Hand), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = isSelected, + onCheckedChange = { checked -> + onToggleDirective( + if (checked) directive.id else null, + ) + }, + modifier = Modifier + .size(36.dp) + .pointerHoverIcon(PointerIcon.Hand), + ) + Text( + text = directive.name, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (directive.scope == DirectiveScope.TEAM) { + Spacer(modifier = Modifier.width(4.dp)) + Surface( + shape = MaterialTheme.shapes.extraSmall, + color = MaterialTheme.colorScheme.tertiaryContainer, + ) { + Text( + text = stringResource("directive.scope.team"), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.padding( + horizontal = 4.dp, + vertical = 2.dp, + ), + ) + } + } + } + } // end themedRichTooltip content + } + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + + DropdownMenuItem( + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Default.Add, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource("chat.directive.new"), + style = MaterialTheme.typography.bodyMedium, + ) + } + }, + onClick = { + showNewDirectiveDialog = true + directivePopupExpanded = false + }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + + DropdownMenuItem( + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Default.Edit, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource("chat.directive.manage"), + style = MaterialTheme.typography.bodyMedium, + ) + } + }, + onClick = { + showManageDirectivesDialog = true + directivePopupExpanded = false + }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + val uriHandler = LocalUriHandler.current + DropdownMenuItem( + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Default.ChevronRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp), + ) + Text( + text = stringResource("chat.directive.learn.more"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + onClick = { + uriHandler.openUri( + "https://$DOMAIN/docs/desktop/directives/", + ) + directivePopupExpanded = false + }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + } + } + } + } + } + } + // Image mode chip — only show when user explicitly toggles to Image mode // and the model requires explicit toggle (not native image generation) if (creationMode is CreationMode.Image && !supportsNativeImageGeneration) { @@ -926,6 +1285,38 @@ fun chatInputField( } } } + + // ── Directive dialogs ──────────────────────────────────────────────────── + // Owned here so the chip is their natural trigger point. + if (showNewDirectiveDialog) { + newDirectiveDialog( + onDismiss = { showNewDirectiveDialog = false }, + onConfirm = { name, content, applyToCurrent -> + onDirectiveCreated?.invoke(name, content, applyToCurrent) + showNewDirectiveDialog = false + }, + ) + } + + if (showManageDirectivesDialog && onDirectiveUpdated != null) { + manageDirectivesDialog( + directives = availableDirectives, + onDismiss = { showManageDirectivesDialog = false }, + onAdd = { name, content, applyToCurrent -> + onDirectiveCreated?.invoke(name, content, applyToCurrent) + }, + onUpdate = { id, newName, newContent -> + onDirectiveUpdated.invoke(id, newName, newContent) + }, + onDelete = { id -> + onDirectiveDeleted?.invoke(id) + }, + onExport = { onDirectiveExported?.invoke() ?: "" }, + onImport = { json -> + onDirectiveImported?.invoke(json) ?: DirectiveImportResult(0, 0, 0) + }, + ) + } } /** diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt index 37204707..3b88dca3 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt @@ -13,27 +13,21 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.AutoAwesome -import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Search @@ -41,7 +35,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -82,9 +75,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.askimo.core.AppConstants.DOMAIN import io.askimo.core.chat.domain.ChatDirective -import io.askimo.core.chat.domain.DirectiveScope import io.askimo.core.chat.domain.Project import io.askimo.core.chat.dto.ChatMessageDTO import io.askimo.core.chat.dto.FileAttachmentDTO @@ -107,16 +98,12 @@ import io.askimo.ui.common.keymap.KeyMapManager import io.askimo.ui.common.keymap.KeyMapManager.AppShortcut import io.askimo.ui.common.preferences.ApplicationPreferences import io.askimo.ui.common.theme.AppComponents -import io.askimo.ui.common.theme.AppComponents.dropdownMenu import io.askimo.ui.common.theme.LocalBackgroundActive import io.askimo.ui.common.theme.Spacing import io.askimo.ui.common.theme.ThemePreferences -import io.askimo.ui.common.ui.TooltipPlacement import io.askimo.ui.common.ui.themedTooltip import io.askimo.ui.common.ui.util.FileDialogUtils import io.askimo.ui.service.AvatarService -import io.askimo.ui.session.manageDirectivesDialog -import io.askimo.ui.session.newDirectiveDialog import io.askimo.ui.session.sessionActionsMenu import io.askimo.ui.session.sessionMemoryDialog import kotlinx.coroutines.Dispatchers @@ -281,7 +268,6 @@ fun chatView( // Load all directives var availableDirectives by remember { mutableStateOf>(emptyList()) } - var showNewDirectiveDialog by remember { mutableStateOf(false) } // Session memory dialog state var showSessionMemoryDialog by remember { mutableStateOf(false) } @@ -440,27 +426,6 @@ fun chatView( } } - // Show new directive dialog - if (showNewDirectiveDialog) { - newDirectiveDialog( - onDismiss = { showNewDirectiveDialog = false }, - onConfirm = { name, content, applyToCurrent -> - // Create the new directive - val newDirective = directiveService.createDirective(name, content) - - // Reload directives - availableDirectives = directiveService.listAllDirectives() - - // Apply to current session if requested - if (applyToCurrent) { - actions.setDirective(newDirective.id) - } - - showNewDirectiveDialog = false - }, - ) - } - Box( modifier = Modifier .fillMaxSize() @@ -662,273 +627,11 @@ fun chatView( } } - // Right side: Directive selector and session actions + // Right side: session actions Row( horizontalArrangement = Arrangement.spacedBy(Spacing.small), verticalAlignment = Alignment.CenterVertically, ) { - var showManageDirectivesDialog by remember { mutableStateOf(false) } - var directiveDropdownExpanded by remember { mutableStateOf(false) } - - // Get the selected directive details - val selectedDirectiveObj = remember(selectedDirective, availableDirectives) { - selectedDirective?.let { id -> - availableDirectives.find { it.id == id } - } - } - - Box { - themedTooltip( - text = if (selectedDirectiveObj != null) { - "${stringResource("chat.directive")}: ${selectedDirectiveObj.name}\n${selectedDirectiveObj.content}" - } else { - "${stringResource("chat.directive")}: ${stringResource("chat.directive.none")}" - }, - ) { - TextButton( - onClick = { directiveDropdownExpanded = true }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - colors = ButtonDefaults.textButtonColors( - contentColor = if (selectedDirectiveObj != null) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ), - ) { - Icon( - imageVector = Icons.Default.AutoAwesome, - contentDescription = stringResource("chat.directive"), - modifier = Modifier.size(16.dp), - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = selectedDirectiveObj?.name?.take(30)?.let { - if (selectedDirectiveObj.name.length > 30) "$it..." else it - } ?: stringResource("chat.directive.none"), - style = MaterialTheme.typography.bodyMedium, - ) - Icon( - imageVector = Icons.Default.ArrowDropDown, - contentDescription = "Select directive", - modifier = Modifier.size(20.dp), - ) - } - } - - dropdownMenu( - expanded = directiveDropdownExpanded, - onDismissRequest = { directiveDropdownExpanded = false }, - modifier = Modifier.fillMaxWidth(0.3f), - ) { - // "None" option to clear directive - DropdownMenuItem( - text = { - Text( - text = stringResource("chat.directive.none"), - style = MaterialTheme.typography.bodyMedium, - ) - }, - onClick = { - actions.setDirective(null) - directiveDropdownExpanded = false - }, - leadingIcon = if (selectedDirective == null) { - { - Icon( - Icons.Default.Check, - contentDescription = "Selected", - tint = MaterialTheme.colorScheme.onSurface, - ) - } - } else { - null - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) - - // Show available directives in a scrollable section - if (availableDirectives.isNotEmpty()) { - HorizontalDivider() - - availableDirectives.forEach { directive -> - themedTooltip( - text = "${directive.name}\n${directive.content}", - placement = TooltipPlacement.LEFT, - ) { - DropdownMenuItem( - text = { - Row( - horizontalArrangement = Arrangement.spacedBy(Spacing.small), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = directive.name, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f, fill = false), - ) - if (directive.scope == DirectiveScope.TEAM) { - Surface( - shape = MaterialTheme.shapes.extraSmall, - color = MaterialTheme.colorScheme.tertiaryContainer, - ) { - Text( - text = stringResource("directive.scope.team"), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onTertiaryContainer, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), - ) - } - } - } - }, - onClick = { - actions.setDirective(directive.id) - directiveDropdownExpanded = false - }, - leadingIcon = if (selectedDirective == directive.id) { - { - Icon( - Icons.Default.Check, - contentDescription = "Selected", - tint = MaterialTheme.colorScheme.onSurface, - ) - } - } else { - null - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) - } - } - } - - // Action items section - HorizontalDivider( - modifier = Modifier.padding(vertical = 4.dp), - ) - - // New Directive action - DropdownMenuItem( - text = { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - Icons.Default.Add, - contentDescription = "New directive", - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource("chat.directive.new"), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - }, - onClick = { - showNewDirectiveDialog = true - directiveDropdownExpanded = false - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) - - // Manage Directives action - DropdownMenuItem( - text = { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - Icons.Default.Edit, - contentDescription = "Manage directives", - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource("chat.directive.manage"), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - }, - onClick = { - showManageDirectivesDialog = true - directiveDropdownExpanded = false - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) - - // Learn more link - HorizontalDivider( - modifier = Modifier.padding(vertical = 4.dp), - ) - val uriHandler = androidx.compose.ui.platform.LocalUriHandler.current - DropdownMenuItem( - text = { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - Icons.Default.ChevronRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp), - ) - Text( - text = stringResource("chat.directive.learn.more"), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - }, - onClick = { - uriHandler.openUri("https://$DOMAIN/docs/desktop/directives/") - directiveDropdownExpanded = false - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) - } - - // Show manage directives dialog - if (showManageDirectivesDialog) { - manageDirectivesDialog( - directives = availableDirectives, - onDismiss = { showManageDirectivesDialog = false }, - onAdd = { name, content, applyToCurrent -> - val newDirective = directiveService.createDirective(name, content) - availableDirectives = directiveService.listAllDirectives() - if (applyToCurrent) { - actions.setDirective(newDirective.id) - } - }, - onUpdate = { id, newName, newContent -> - directiveService.updateDirective(id, newName, newContent) - availableDirectives = directiveService.listAllDirectives() - }, - onDelete = { id -> - directiveService.deleteDirective(id) - if (selectedDirective == id) { - actions.setDirective(null) - } - availableDirectives = directiveService.listAllDirectives() - }, - onExport = { - directiveService.exportToJson() - }, - onImport = { json -> - val result = directiveService.importFromJson(json) - availableDirectives = directiveService.listAllDirectives() - result - }, - ) - } - } - if (sessionId != null && messages.isNotEmpty()) { sessionActionsMenu( sessionId = sessionId, @@ -1275,6 +978,30 @@ fun chatView( sessionId = sessionId, onEnabledServerIdsChange = { currentEnabledServerIds = it }, onNavigateToMcpSettings = onNavigateToMcpSettings, + // Directives chip + availableDirectives = availableDirectives, + selectedDirective = selectedDirective, + onToggleDirective = { id -> actions.setDirective(id) }, + onDirectiveCreated = { name, content, applyToCurrent -> + val newDirective = directiveService.createDirective(name, content) + availableDirectives = directiveService.listAllDirectives() + if (applyToCurrent) actions.setDirective(newDirective.id) + }, + onDirectiveUpdated = { id, newName, newContent -> + directiveService.updateDirective(id, newName, newContent) + availableDirectives = directiveService.listAllDirectives() + }, + onDirectiveDeleted = { id -> + directiveService.deleteDirective(id) + if (selectedDirective == id) actions.setDirective(null) + availableDirectives = directiveService.listAllDirectives() + }, + onDirectiveExported = { directiveService.exportToJson() }, + onDirectiveImported = { json -> + val result = directiveService.importFromJson(json) + availableDirectives = directiveService.listAllDirectives() + result + }, modifier = Modifier .widthIn(max = ThemePreferences.CONTENT_MAX_WIDTH) .fillMaxWidth() diff --git a/desktop-shared/src/main/resources/i18n/messages.properties b/desktop-shared/src/main/resources/i18n/messages.properties index e1651ba9..c39dc37a 100644 --- a/desktop-shared/src/main/resources/i18n/messages.properties +++ b/desktop-shared/src/main/resources/i18n/messages.properties @@ -637,8 +637,7 @@ global.search.results.count={0} result found global.search.results.count.plural={0} results found # Rest of chat view strings -chat.directive=Directive: -chat.directive.none=None +chat.directive=Directive chat.directive.new=New Directive chat.directive.manage=Manage Directives chat.directive.learn.more=Learn how to use directives diff --git a/desktop-shared/src/main/resources/i18n/messages_de.properties b/desktop-shared/src/main/resources/i18n/messages_de.properties index 6c89ada6..97ad100d 100644 --- a/desktop-shared/src/main/resources/i18n/messages_de.properties +++ b/desktop-shared/src/main/resources/i18n/messages_de.properties @@ -615,8 +615,7 @@ global.search.results.count={0} Ergebnis gefunden global.search.results.count.plural={0} Ergebnisse gefunden # Rest of chat view strings -chat.directive=Direktive: -chat.directive.none=Keine +chat.directive=Direktive chat.directive.new=Neue Direktive chat.directive.manage=Direktiven verwalten chat.directive.learn.more=Erfahren Sie, wie Sie Direktiven verwenden diff --git a/desktop-shared/src/main/resources/i18n/messages_es.properties b/desktop-shared/src/main/resources/i18n/messages_es.properties index e964354e..47d12409 100644 --- a/desktop-shared/src/main/resources/i18n/messages_es.properties +++ b/desktop-shared/src/main/resources/i18n/messages_es.properties @@ -615,7 +615,6 @@ global.search.results.count.plural={0} resultados encontrados # Rest of chat view strings chat.directive=Directiva: -chat.directive.none=Ninguna chat.directive.new=Nueva directiva chat.directive.manage=Gestionar directivas chat.directive.learn.more=Aprenda a usar directivas diff --git a/desktop-shared/src/main/resources/i18n/messages_fr.properties b/desktop-shared/src/main/resources/i18n/messages_fr.properties index 9a8d527b..1275bbe7 100644 --- a/desktop-shared/src/main/resources/i18n/messages_fr.properties +++ b/desktop-shared/src/main/resources/i18n/messages_fr.properties @@ -614,8 +614,7 @@ global.search.results.count={0} résultat trouvé global.search.results.count.plural={0} résultats trouvés # Rest of chat view strings -chat.directive=Directive : -chat.directive.none=Aucune +chat.directive=Directive chat.directive.new=Nouvelle directive chat.directive.manage=Gérer les directives chat.directive.learn.more=Apprenez à utiliser les directives 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 82dd111e..ad9a4757 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties @@ -613,8 +613,7 @@ global.search.results.count={0}件の結果 global.search.results.count.plural={0}件の結果 # Rest of chat view strings -chat.directive=ディレクティブ: -chat.directive.none=なし +chat.directive=ディレクティブ chat.directive.new=新しいディレクティブ chat.directive.manage=ディレクティブ管理 chat.directive.learn.more=ディレクティブの使い方を学ぶ 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 aea4cc0f..38fe952d 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties @@ -614,8 +614,7 @@ global.search.results.count={0}개 결과 global.search.results.count.plural={0}개 결과 # Rest of chat view strings -chat.directive=지시문: -chat.directive.none=없음 +chat.directive=지시문 chat.directive.new=새 지시문 chat.directive.manage=지시문 관리 chat.directive.learn.more=디렉티브 사용법을 배워보세요 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 fa2beda1..8103a94d 100644 --- a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties @@ -616,8 +616,7 @@ global.search.results.count={0} resultado encontrado global.search.results.count.plural={0} resultados encontrados # Rest of chat view strings -chat.directive=Diretiva: -chat.directive.none=Nenhuma +chat.directive=Diretiva chat.directive.new=Nova Diretiva chat.directive.manage=Gerenciar Diretivas chat.directive.learn.more=Aprenda a usar diretivas 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 e01f0d94..701e2312 100644 --- a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties @@ -621,8 +621,7 @@ global.search.results.count={0} kết quả global.search.results.count.plural={0} kết quả # Rest of chat view strings -chat.directive=Chỉ thị: -chat.directive.none=Không có +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ị 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 3b88d19a..4ecb6237 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties @@ -614,8 +614,7 @@ global.search.results.count=找到{0}个结果 global.search.results.count.plural=找到{0}个结果 # Rest of chat view strings -chat.directive=指令: -chat.directive.none=无 +chat.directive=指令 chat.directive.new=新指令 chat.directive.manage=管理指令 chat.directive.learn.more=了解如何使用指令 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 541b9168..1bab4bbc 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties @@ -613,8 +613,7 @@ global.search.results.count=找到{0}個結果 global.search.results.count.plural=找到{0}個結果 # Rest of chat view strings -chat.directive=指令: -chat.directive.none=無 +chat.directive=指令 chat.directive.new=新增指令 chat.directive.manage=管理指令 chat.directive.learn.more=了解如何使用指令