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
2 changes: 1 addition & 1 deletion .github/workflows/cli-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ jobs:
with:
files: ${{ steps.pkg.outputs.files }}
generate_release_notes: true
draft: false
draft: true
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,71 @@ jobs:
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}



contributors:
name: Append contributors to release notes
if: startsWith(github.ref, 'refs/tags/v')
needs: [ release ]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout (full history for git log)
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Build contributors section and append to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
TAG="${{ github.ref_name }}"

# Find the previous tag (the one before this one)
PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${TAG}$" | head -n1 || true)

if [ -n "$PREV_TAG" ]; then
RANGE="${PREV_TAG}..${TAG}"
echo "Contributors from $RANGE"
else
RANGE="${TAG}"
echo "No previous tag found — listing all contributors up to $TAG"
fi

# Collect unique authors: "Login (Name)" sorted, bots excluded
mapfile -t AUTHORS < <(
git log "$RANGE" --format='%aN|%aE' \
| sort -u \
| grep -viE '\[bot\]|noreply\.github\.com' \
| awk -F'|' '{print $1}' \
| sort -u
)

if [ ${#AUTHORS[@]} -eq 0 ]; then
echo "No human contributors found — skipping append."
exit 0
fi

echo "Found ${#AUTHORS[@]} contributor(s): ${AUTHORS[*]}"

# Build markdown section
SECTION=$'\n\n## Contributors\n\nThank you to everyone who contributed to this release! 🎉\n\n'
for AUTHOR in "${AUTHORS[@]}"; do
SECTION+="- ${AUTHOR}"$'\n'
done

# Fetch current release body
CURRENT_BODY=$(gh release view "$TAG" --json body --jq '.body' 2>/dev/null || echo "")

# Remove any previously appended Contributors section (idempotent re-runs)
STRIPPED=$(echo "$CURRENT_BODY" | sed '/^## Contributors/,$ d')

NEW_BODY="${STRIPPED}${SECTION}"

gh release edit "$TAG" --notes "$NEW_BODY"
echo "✅ Contributors section appended to release $TAG"

183 changes: 146 additions & 37 deletions desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ 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.Image
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
Expand Down Expand Up @@ -75,6 +77,7 @@ import androidx.compose.ui.window.Popup
import io.askimo.core.chat.dto.ChatMessageDTO
import io.askimo.core.chat.dto.FileAttachmentDTO
import io.askimo.core.chat.service.ChatSessionService
import io.askimo.core.chat.util.FileContentExtractor
import io.askimo.core.config.AppConfig
import io.askimo.core.event.EventBus
import io.askimo.core.event.error.AppErrorEvent
Expand Down Expand Up @@ -1104,53 +1107,159 @@ private fun fileAttachmentItem(
attachment: FileAttachmentDTO,
onRemove: () -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
var previewContent by remember { mutableStateOf<String?>(null) }
var isLoadingPreview by remember { mutableStateOf(false) }

// Determine if the file is previewable as text — delegate to FileContentExtractor
val isTextFile = FileContentExtractor.isTextFile(attachment.fileName)

LaunchedEffect(expanded) {
if (expanded && isTextFile && previewContent == null && !isLoadingPreview) {
isLoadingPreview = true
previewContent = withContext(Dispatchers.IO) {
try {
val file = attachment.filePath?.let { java.io.File(it) }
if (file != null && file.exists() && file.length() < 512 * 1024) {
// Read up to 200 lines for preview
file.bufferedReader().use { reader ->
val lines = reader.readLines()
val preview = lines.take(200).joinToString("\n")
if (lines.size > 200) {
"$preview\n… (${LocalizationManager.getString("chat.attachment.preview.more.lines", lines.size - 200)})"
} else {
preview
}
}
} else if (file != null && !file.exists()) {
LocalizationManager.getString("chat.attachment.preview.file.not.found")
} else {
LocalizationManager.getString("chat.attachment.preview.too.large")
}
} catch (e: Exception) {
LocalizationManager.getString("chat.attachment.preview.cannot.read", e.message ?: "")
}
}
isLoadingPreview = false
}
}

Card(
modifier = Modifier.fillMaxWidth(),
colors = AppComponents.surfaceVariantCardColors(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.fillMaxWidth()) {
// Header row
Row(
modifier = Modifier
.fillMaxWidth()
.then(
if (isTextFile) {
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { expanded = !expanded },
).pointerHoverIcon(PointerIcon.Hand)
} else {
Modifier
},
)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Icon(
imageVector = Icons.Default.AttachFile,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Column {
Text(
text = attachment.fileName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = formatFileSize(attachment.size),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.weight(1f),
) {
Icon(
imageVector = Icons.Default.AttachFile,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Column {
Text(
text = attachment.fileName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = formatFileSize(attachment.size),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (isTextFile) {
Icon(
imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = if (expanded) "Collapse preview" else "Expand preview",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
IconButton(
onClick = onRemove,
modifier = Modifier
.size(24.dp)
.pointerHoverIcon(PointerIcon.Hand),
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource("chat.attachment.remove"),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
IconButton(
onClick = onRemove,
modifier = Modifier
.size(24.dp)
.pointerHoverIcon(PointerIcon.Hand),
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource("chat.attachment.remove"),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)

// Inline preview panel
if (expanded && isTextFile) {
HorizontalDivider()
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 240.dp)
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.5f))
.padding(horizontal = 12.dp, vertical = 8.dp),
) {
when {
isLoadingPreview -> {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator(modifier = Modifier.size(14.dp), strokeWidth = 2.dp)
Text(
text = stringResource("chat.attachment.preview.loading"),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

previewContent != null -> {
val scrollState = rememberScrollState()
androidx.compose.foundation.text.selection.SelectionContainer {
Text(
text = previewContent!!,
style = MaterialTheme.typography.labelSmall.copy(
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.verticalScroll(scrollState),
)
}
}
}
}
}
}
}
Expand Down
37 changes: 33 additions & 4 deletions desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import kotlinx.coroutines.withContext
import org.koin.core.context.GlobalContext
import java.io.File
import java.time.LocalDateTime
import java.util.UUID.randomUUID

private val log = currentFileLogger()

Expand Down Expand Up @@ -149,10 +150,16 @@ fun chatView(

// Internal state management for ChatView
val scope = rememberCoroutineScope()
var inputText by remember(initialInputText) { mutableStateOf(initialInputText) }
var attachments by remember(initialAttachments) { mutableStateOf(initialAttachments) }
var editingMessage by remember(initialEditingMessage) { mutableStateOf(initialEditingMessage) }
var editingAIMessage by remember { mutableStateOf<ChatMessageDTO?>(null) }
var inputText by remember(sessionId, initialInputText) { mutableStateOf(initialInputText) }
var attachments by remember(sessionId, initialAttachments) { mutableStateOf(initialAttachments) }
var editingMessage by remember(sessionId, initialEditingMessage) { mutableStateOf(initialEditingMessage) }
var editingAIMessage by remember(sessionId) { mutableStateOf<ChatMessageDTO?>(null) }

// When there is no project (e.g. user clicked "New Chat"), drop any pending
// attachments that were added from the project side panel.
LaunchedEffect(project) {
if (project == null) attachments = emptyList()
}

// Notify parent of state changes
LaunchedEffect(inputText, attachments, editingMessage) {
Expand Down Expand Up @@ -1086,6 +1093,28 @@ fun chatView(
ragIndexingPercentage = ragIndexingPercentage,
isExpanded = sidePanelExpanded,
onExpandedChange = { sidePanelExpanded = it },
onAddToChat = { filePaths ->
val newAttachments = filePaths.map { path ->
val file = File(path)
FileAttachmentDTO(
id = randomUUID().toString(),
messageId = "",
sessionId = sessionId ?: "",
fileName = file.name,
mimeType = file.extension,
size = file.length(),
createdAt = LocalDateTime.now(),
content = null,
filePath = file.absolutePath,
)
}
// Merge, avoiding duplicates by path
val existingPaths = attachments.mapNotNull { it.filePath }.toSet()
val toAdd = newAttachments.filter { it.filePath !in existingPaths }
if (toAdd.isNotEmpty()) {
attachments = attachments + toAdd
}
},
modifier = Modifier.fillMaxHeight(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -161,7 +161,7 @@ fun fileViewerPane(
modifier = Modifier.size(28.dp).pointerHoverIcon(PointerIcon.Hand),
) {
Icon(
imageVector = Icons.Default.OpenInNew,
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = stringResource("file.viewer.open.external"),
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(15.dp),
Expand Down Expand Up @@ -252,7 +252,7 @@ fun fileViewerPane(
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(32.dp),
)
},
Expand Down Expand Up @@ -322,10 +322,15 @@ private fun viewerPlaceholder(
Icon(
imageVector = Icons.Default.FolderOpen,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(15.dp),
)
Spacer(Modifier.size(6.dp))
Text(text = actionLabel, style = MaterialTheme.typography.bodySmall)
Text(
text = actionLabel,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
Expand Down
Loading
Loading