mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
better terminal output? trying to make AUR release work, handle .kcppt saving
This commit is contained in:
parent
a49e40bb86
commit
1c010a7e88
13 changed files with 157 additions and 94 deletions
72
.github/RELEASE.md
vendored
72
.github/RELEASE.md
vendored
|
|
@ -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
|
|
||||||
24
.github/workflows/aur-release.yml
vendored
24
.github/workflows/aur-release.yml
vendored
|
|
@ -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: |
|
||||||
|
|
|
||||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
53
README.md
53
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
screenshots/download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
BIN
screenshots/gen-img.png
Normal file
BIN
screenshots/gen-img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
BIN
screenshots/launch.png
Normal file
BIN
screenshots/launch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
screenshots/terminal.png
Normal file
BIN
screenshots/terminal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
BIN
screenshots/text-story.png
Normal file
BIN
screenshots/text-story.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue