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 + +Download Interface + +### Model Launch Configuration + +Launch Configuration + +### Terminal Output + +Terminal Interface + +### Text Generation + +Text Story Generation + +### Image 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;