better terminal output? trying to make AUR release work, handle .kcppt saving

This commit is contained in:
Egor 2025-08-22 19:52:26 -07:00
parent a49e40bb86
commit 1c010a7e88
13 changed files with 157 additions and 94 deletions

72
.github/RELEASE.md vendored
View file

@ -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

View file

@ -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: |

View file

@ -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 }}

View file

@ -13,20 +13,9 @@ A modern desktop app for running Large Language Models locally. <!-- markdownlin
- **Smart process management** - Prevents runaway background processes and system resource waste
- **Optimized performance** - Automatically unpacks binaries for faster operation and reduced memory usage (up to ~4GB less RAM)
- **Image generation support** - Built-in presets for Flux and Chroma image generation workflows
- **Adaptive theming** - Light, dark, and system theme modes that automatically follow your OS preferences
- **Privacy-focused** - Everything runs locally on your machine, no data sent to external servers
### 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 modern day Vulkan 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.
## Installation
### Pre-built Binaries (Recommended)
@ -62,6 +51,46 @@ makepkg -si
The AUR package automatically handles installation, desktop integration, and system updates. This is the ideal way to run Friendly Kobold on Linux.
## Screenshots
<!-- markdownlint-disable MD033 -->
<div align="center">
### Download & Setup
<img src="screenshots/download.png" alt="Download Interface" width="600">
### Model Launch Configuration
<img src="screenshots/launch.png" alt="Launch Configuration" width="600">
### Terminal Output
<img src="screenshots/terminal.png" alt="Terminal Interface" width="600">
### Text Generation
<img src="screenshots/text-story.png" alt="Text Story Generation" width="600">
### Image Generation
<img src="screenshots/gen-img.png" alt="Image Generation" width="600">
</div>
<!-- markdownlint-enable MD033 -->
### 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

View file

@ -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/**/*"
]
},
{

BIN
screenshots/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
screenshots/gen-img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
screenshots/launch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
screenshots/terminal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

BIN
screenshots/text-story.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View file

@ -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<string>('');
@ -24,6 +78,21 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
const scrollAreaRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
const lastScrollTop = useRef<number>(0);
const updateTimeoutRef = useRef<number | null>(null);
const pendingContentRef = useRef<string>('');
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) {

View file

@ -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,

View file

@ -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;