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(`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: |
|
||||
|
|
|
|||
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
|
||||
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 }}
|
||||
|
|
|
|||
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
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -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
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 {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue