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(`Author: ${authorName} <${authorEmail}>`);
console.log(`AppImage URL: ${appImageUrl}`); 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 - name: Download AppImage and calculate SHA256
id: sha_calc id: sha_calc
run: | run: |

View file

@ -115,11 +115,10 @@ jobs:
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi fi
- name: Create Draft Release - name: Create Release
run: | run: |
gh release create ${{ steps.tag.outputs.tag }} \ gh release create ${{ steps.tag.outputs.tag }} \
--title "FriendlyKobold ${{ steps.tag.outputs.tag }}" \ --title "FriendlyKobold ${{ steps.tag.outputs.tag }}" \
--draft \
--generate-notes --generate-notes
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 - **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) - **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 - **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 - **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 ## Installation
### Pre-built Binaries (Recommended) ### 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. 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 ## For Local Dev
### Prerequisites ### Prerequisites

View file

@ -1,7 +1,7 @@
{ {
"name": "friendly-kobold", "name": "friendly-kobold",
"productName": "Friendly Kobold", "productName": "Friendly Kobold",
"version": "0.5.5", "version": "0.5.6",
"description": "A modern desktop app for running Large Language Models locally", "description": "A modern desktop app for running Large Language Models locally",
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",
@ -121,7 +121,8 @@
"from": "assets", "from": "assets",
"to": "assets", "to": "assets",
"filter": [ "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 { import {
Box, Box,
ScrollArea, ScrollArea,
@ -15,6 +15,60 @@ interface TerminalTabProps {
onServerReady?: (serverUrl: string) => void; 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) => { export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const [terminalContent, setTerminalContent] = useState<string>(''); const [terminalContent, setTerminalContent] = useState<string>('');
@ -24,6 +78,21 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
const lastScrollTop = useRef<number>(0); 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( const converter = useRef(
new Convert({ 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; return cleanup;
}, [onServerReady]); }, [onServerReady, debouncedUpdateContent]);
const scrollToBottom = () => { const scrollToBottom = () => {
if (viewportRef.current) { if (viewportRef.current) {

View file

@ -202,7 +202,7 @@ export const LaunchScreen = ({
} }
try { try {
const configName = selectedFile.replace('.kcpps', ''); const configName = selectedFile.replace(/\.(kcpps|kcppt)$/, '');
const success = await window.electronAPI.kobold.saveConfigFile( const success = await window.electronAPI.kobold.saveConfigFile(
configName, configName,

View file

@ -358,8 +358,14 @@ export class KoboldCppManager {
return false; return false;
} }
const configFileName = `${configName}.kcpps`; let configFileName = `${configName}.kcpps`;
const configPath = join(this.installDir, configFileName); 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'); writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
return true; return true;