diff --git a/.github/RELEASE.md b/.github/RELEASE.md
deleted file mode 100644
index 7fb16fb..0000000
--- a/.github/RELEASE.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Release Workflow
-
-This GitHub Action workflow automatically builds and releases FriendlyKobold for macOS, Windows, and Linux.
-
-## How to Create a Release
-
-### Method 1: Tag-based Release (Recommended)
-
-1. Create and push a new tag:
-
- ```bash
- git tag v1.0.0
- git push origin v1.0.0
- ```
-
-2. The workflow will automatically:
- - Build the app for all platforms
- - Run type checking and linting
- - Create a GitHub release
- - Upload the built files as release assets
-
-### Method 2: Manual Release
-
-1. Go to the "Actions" tab in your GitHub repository
-2. Select the "Release" workflow
-3. Click "Run workflow"
-4. Enter the tag version (e.g., `v1.0.0`)
-5. Click "Run workflow"
-
-## Generated Files
-
-The workflow creates the following files:
-
-- **macOS**: `FriendlyKobold-{version}.dmg`
-- **Windows**: `FriendlyKobold Setup {version}.exe`
-- **Linux**: `FriendlyKobold-{version}.AppImage`
-
-## Requirements
-
-- The repository must have a `GITHUB_TOKEN` (automatically provided by GitHub)
-- Node.js 22 and Yarn for building
-- All dependencies must be properly defined in `package.json`
-
-## Troubleshooting
-
-If the build fails:
-
-1. Check that all dependencies are installed correctly
-2. Ensure the build script works locally: `yarn build`
-3. Check the workflow logs for specific error messages
-4. Verify that the version tag follows semantic versioning (e.g., `v1.0.0`)
-
-## Adding Icons
-
-To customize the app icon:
-
-1. Replace `assets/icon.png` with your custom 512x512 PNG icon
-2. Electron Builder automatically converts this single PNG to the appropriate format for each platform:
- - macOS: Converts to `.icns` format
- - Windows: Converts to `.ico` format
- - Linux: Uses PNG directly
-
-The icon is already configured in `package.json` under the `build.icon` property.
-
-## Customizing the Release
-
-You can customize the release by editing `.github/workflows/release.yml`:
-
-- Change the supported platforms in the `matrix.os` array
-- Modify the release notes template
-- Add additional build steps or checks
-- Change the artifact upload logic
diff --git a/.github/workflows/aur-release.yml b/.github/workflows/aur-release.yml
index 6cdd92f..9cdbd25 100644
--- a/.github/workflows/aur-release.yml
+++ b/.github/workflows/aur-release.yml
@@ -54,6 +54,30 @@ jobs:
console.log(`Author: ${authorName} <${authorEmail}>`);
console.log(`AppImage URL: ${appImageUrl}`);
+ - name: Verify AppImage availability
+ run: |
+ echo "Verifying AppImage is accessible..."
+ APPIMAGE_URL="${{ steps.release_info.outputs.appimage_url }}"
+ max_attempts=10
+ attempt=1
+
+ while [ $attempt -le $max_attempts ]; do
+ echo "Attempt $attempt: Checking if AppImage is available..."
+ if curl -I -f "$APPIMAGE_URL" > /dev/null 2>&1; then
+ echo "✅ AppImage is accessible"
+ break
+ else
+ echo "❌ AppImage not yet accessible, waiting 30 seconds..."
+ sleep 30
+ attempt=$((attempt + 1))
+ fi
+ done
+
+ if [ $attempt -gt $max_attempts ]; then
+ echo "❌ AppImage not accessible after $max_attempts attempts"
+ exit 1
+ fi
+
- name: Download AppImage and calculate SHA256
id: sha_calc
run: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ed83746..375ff5f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -115,11 +115,10 @@ jobs:
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- - name: Create Draft Release
+ - name: Create Release
run: |
gh release create ${{ steps.tag.outputs.tag }} \
--title "FriendlyKobold ${{ steps.tag.outputs.tag }}" \
- --draft \
--generate-notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index 470dadf..cdb33c8 100644
--- a/README.md
+++ b/README.md
@@ -13,20 +13,9 @@ A modern desktop app for running Large Language Models locally.
+
+
+### Download & Setup
+
+

+
+### Model Launch Configuration
+
+

+
+### Terminal Output
+
+

+
+### Text Generation
+
+

+
+### Image Generation
+
+

+
+
+
+
+### Windows ROCm Support
+
+There is ROCm Windows support maintained by YellowRoseCx in a separate fork.
+Unfortunately it does not properly support unpacking, which would greatly diminish its performance and provide a poor UX when used alongside this app.
+For Friendly Kobold to work with this fork, [this issue must be fixed first](https://github.com/YellowRoseCx/koboldcpp-rocm/issues/129).
+
+Note that this build is not important as the modern day Vulkan backend matches or even surpasses ROCm in terms of LLM performance for most cases.
+
+### Future features
+
+Not all koboldcpp features have currently been ported over the UI. As a workaround one may use the "Additional arguments" on the "Advanced" tab of the launcher to provide additional command line arguments if you know them.
+
## For Local Dev
### Prerequisites
diff --git a/package.json b/package.json
index 4dc32e8..89cbc55 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "friendly-kobold",
"productName": "Friendly Kobold",
- "version": "0.5.5",
+ "version": "0.5.6",
"description": "A modern desktop app for running Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
@@ -121,7 +121,8 @@
"from": "assets",
"to": "assets",
"filter": [
- "**/*"
+ "**/*",
+ "!screenshots/**/*"
]
},
{
diff --git a/screenshots/download.png b/screenshots/download.png
new file mode 100644
index 0000000..2f0fd1c
Binary files /dev/null and b/screenshots/download.png differ
diff --git a/screenshots/gen-img.png b/screenshots/gen-img.png
new file mode 100644
index 0000000..2a0dd63
Binary files /dev/null and b/screenshots/gen-img.png differ
diff --git a/screenshots/launch.png b/screenshots/launch.png
new file mode 100644
index 0000000..9e11d8a
Binary files /dev/null and b/screenshots/launch.png differ
diff --git a/screenshots/terminal.png b/screenshots/terminal.png
new file mode 100644
index 0000000..23def94
Binary files /dev/null and b/screenshots/terminal.png differ
diff --git a/screenshots/text-story.png b/screenshots/text-story.png
new file mode 100644
index 0000000..c879a7f
Binary files /dev/null and b/screenshots/text-story.png differ
diff --git a/src/components/screens/Interface/TerminalTab.tsx b/src/components/screens/Interface/TerminalTab.tsx
index 1648120..944613c 100644
--- a/src/components/screens/Interface/TerminalTab.tsx
+++ b/src/components/screens/Interface/TerminalTab.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from 'react';
+import { useState, useEffect, useRef, useCallback } from 'react';
import {
Box,
ScrollArea,
@@ -15,6 +15,60 @@ interface TerminalTabProps {
onServerReady?: (serverUrl: string) => void;
}
+const handleCarriageReturns = (
+ prevContent: string,
+ newData: string
+): string => {
+ if (!newData.includes('\r')) {
+ return prevContent + newData;
+ }
+
+ try {
+ let result = prevContent;
+ let i = 0;
+
+ while (i < newData.length) {
+ const char = newData[i];
+
+ if (char === '\r') {
+ const nextChar = newData[i + 1];
+
+ if (nextChar === '\n') {
+ result += '\n';
+ i += 2;
+ } else {
+ const lines = result.split('\n');
+ if (lines.length > 0) {
+ const lastLineIndex = lines.length - 1;
+ const restOfData = newData.slice(i + 1);
+ const nextCrOrLfIndex = restOfData.search(/[\r\n]/);
+
+ if (nextCrOrLfIndex === -1) {
+ lines[lastLineIndex] = restOfData;
+ result = lines.join('\n');
+ break;
+ } else {
+ const replacement = restOfData.slice(0, nextCrOrLfIndex);
+ lines[lastLineIndex] = replacement;
+ result = lines.join('\n');
+ i += 1 + nextCrOrLfIndex;
+ }
+ } else {
+ i++;
+ }
+ }
+ } else {
+ result += char;
+ i++;
+ }
+ }
+
+ return result;
+ } catch {
+ return prevContent + newData;
+ }
+};
+
export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
const { colorScheme } = useMantineColorScheme();
const [terminalContent, setTerminalContent] = useState('');
@@ -24,6 +78,21 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
const scrollAreaRef = useRef(null);
const viewportRef = useRef(null);
const lastScrollTop = useRef(0);
+ const updateTimeoutRef = useRef(null);
+ const pendingContentRef = useRef('');
+
+ const debouncedUpdateContent = useCallback((newContent: string) => {
+ pendingContentRef.current = newContent;
+
+ if (updateTimeoutRef.current) {
+ clearTimeout(updateTimeoutRef.current);
+ }
+
+ updateTimeoutRef.current = window.setTimeout(() => {
+ setTerminalContent(pendingContentRef.current);
+ updateTimeoutRef.current = null;
+ }, 16);
+ }, []);
const converter = useRef(
new Convert({
@@ -96,12 +165,19 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
}
}
- return prev + newData;
+ const newContent = handleCarriageReturns(prev, newData);
+
+ if (newData.includes('\r') && !newData.includes('\n')) {
+ debouncedUpdateContent(newContent);
+ return prev;
+ }
+
+ return newContent;
});
});
return cleanup;
- }, [onServerReady]);
+ }, [onServerReady, debouncedUpdateContent]);
const scrollToBottom = () => {
if (viewportRef.current) {
diff --git a/src/components/screens/Launch/index.tsx b/src/components/screens/Launch/index.tsx
index fd65056..4df9156 100644
--- a/src/components/screens/Launch/index.tsx
+++ b/src/components/screens/Launch/index.tsx
@@ -202,7 +202,7 @@ export const LaunchScreen = ({
}
try {
- const configName = selectedFile.replace('.kcpps', '');
+ const configName = selectedFile.replace(/\.(kcpps|kcppt)$/, '');
const success = await window.electronAPI.kobold.saveConfigFile(
configName,
diff --git a/src/main/managers/KoboldCppManager.ts b/src/main/managers/KoboldCppManager.ts
index 79c0126..b1c19fc 100644
--- a/src/main/managers/KoboldCppManager.ts
+++ b/src/main/managers/KoboldCppManager.ts
@@ -358,8 +358,14 @@ export class KoboldCppManager {
return false;
}
- const configFileName = `${configName}.kcpps`;
- const configPath = join(this.installDir, configFileName);
+ let configFileName = `${configName}.kcpps`;
+ let configPath = join(this.installDir, configFileName);
+
+ const kcpptPath = join(this.installDir, `${configName}.kcppt`);
+ if (existsSync(kcpptPath)) {
+ configFileName = `${configName}.kcppt`;
+ configPath = kcpptPath;
+ }
writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
return true;