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
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ fun chatInputField(
// Session-scoped web-search-in-RAG toggle. Resets when the session changes.
var webSearchInRag by remember(sessionId) { mutableStateOf(false) }

// Cache web-search enabled flag — AppConfig.webSearch hits the OS keychain on every
// call (SecureKeyManager.retrieveSecretKey). Read off the UI thread so the first
// composition is never blocked (avoids navigation lag into ProjectView).
var webSearchEnabled by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
webSearchEnabled = withContext(Dispatchers.IO) { AppConfig.webSearch.enabled }
}

// Notify caller whenever the user changes the enabled server selection.
LaunchedEffect(enabledServerIds) {
onEnabledServerIdsChange?.invoke(enabledServerIds)
Expand Down Expand Up @@ -789,7 +797,7 @@ fun chatInputField(
)

// ── Web search in RAG chip — only in project sessions when web search is configured ──
if (isProjectSession && AppConfig.webSearch.enabled) {
if (isProjectSession && webSearchEnabled) {
Spacer(modifier = Modifier.width(4.dp))
webSearchRagChip(
active = webSearchInRag,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ object AppComponents {
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.padding(top = sectionSpacing, bottom = sectionSpacing)
.padding(end = dialogScrollbarPadding)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(sectionSpacing),
Expand Down
102 changes: 62 additions & 40 deletions desktop/src/main/kotlin/io/askimo/desktop/project/ProjectView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,6 @@ fun projectView(
}

// Local UI state
var inputText by remember { mutableStateOf(TextFieldValue("")) }
var attachments by remember { mutableStateOf<List<FileAttachmentDTO>>(emptyList()) }
var currentEnabledServerIds by remember { mutableStateOf(emptySet<String>()) }
var selectedDirective by remember { mutableStateOf<String?>(null) }
var webSearchInRag by remember { mutableStateOf(false) }
var showProjectMenu by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
var showReIndexConfirmDialog by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -353,41 +348,14 @@ fun projectView(
}
} // end scrollable content column

// Fixed chat input footer — width-constrained, outside scroll
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopCenter,
) {
Column(
modifier = Modifier
.widthIn(max = ThemePreferences.CONTENT_MAX_WIDTH)
.fillMaxWidth()
.padding(start = 24.dp, end = 36.dp, bottom = 24.dp),
) {
chatInputField(
inputText = inputText,
onInputTextChange = { inputText = it },
attachments = attachments,
onAttachmentsChange = { attachments = it },
onSendMessage = { mode ->
if (inputText.text.isNotBlank()) {
onStartChat(currentProject.id, mode, inputText.text, attachments, currentEnabledServerIds, selectedDirective, webSearchInRag)
inputText = TextFieldValue("")
attachments = emptyList()
}
},
onEnabledServerIdsChange = { currentEnabledServerIds = it },
onNavigateToMcpSettings = onNavigateToMcpSettings,
selectedDirective = selectedDirective,
onToggleDirective = { selectedDirective = it },
isProjectSession = true,
onWebSearchInRagChange = { webSearchInRag = it },
sessionId = currentProject.id,
placeholder = stringResource("project.new.chat.placeholder", currentProject.name),
modifier = Modifier.padding(top = Spacing.large),
)
}
}
// Fixed chat input footer — isolated composable so keystrokes only
// recompose the footer, not the entire project view.
projectChatInputFooter(
projectId = currentProject.id,
projectName = currentProject.name,
onStartChat = onStartChat,
onNavigateToMcpSettings = onNavigateToMcpSettings,
)
} // end outer Column

VerticalScrollbar(
Expand Down Expand Up @@ -1102,3 +1070,57 @@ private fun skippedFilesWarning(skippedFileNames: List<String>) {
}
}
}

/**
* Isolated footer composable that owns all chat-input state.
* Keeping state here prevents keystrokes from recomposing the heavy scrollable
* content above (hero card, knowledge sources panel, session cards).
*/
@Composable
private fun projectChatInputFooter(
projectId: String,
projectName: String,
onStartChat: (projectId: String, mode: CreationMode, message: String, attachments: List<FileAttachmentDTO>, enabledServerIds: Set<String>, directiveId: String?, useWebSearch: Boolean) -> Unit,
onNavigateToMcpSettings: (() -> Unit)? = null,
) {
var inputText by remember { mutableStateOf(TextFieldValue("")) }
var attachments by remember { mutableStateOf<List<FileAttachmentDTO>>(emptyList()) }
var currentEnabledServerIds by remember { mutableStateOf(emptySet<String>()) }
var selectedDirective by remember { mutableStateOf<String?>(null) }
var webSearchInRag by remember { mutableStateOf(false) }

Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopCenter,
) {
Column(
modifier = Modifier
.widthIn(max = ThemePreferences.CONTENT_MAX_WIDTH)
.fillMaxWidth()
.padding(start = 24.dp, end = 36.dp, bottom = 24.dp),
) {
chatInputField(
inputText = inputText,
onInputTextChange = { inputText = it },
attachments = attachments,
onAttachmentsChange = { attachments = it },
onSendMessage = { mode ->
if (inputText.text.isNotBlank()) {
onStartChat(projectId, mode, inputText.text, attachments, currentEnabledServerIds, selectedDirective, webSearchInRag)
inputText = TextFieldValue("")
attachments = emptyList()
}
},
onEnabledServerIdsChange = { currentEnabledServerIds = it },
onNavigateToMcpSettings = onNavigateToMcpSettings,
selectedDirective = selectedDirective,
onToggleDirective = { selectedDirective = it },
isProjectSession = true,
onWebSearchInRagChange = { webSearchInRag = it },
sessionId = projectId,
placeholder = stringResource("project.new.chat.placeholder", projectName),
modifier = Modifier.padding(top = Spacing.large),
)
}
}
}
Loading