back to xterm, correct AUR install path

This commit is contained in:
Egor 2025-08-22 22:55:42 -07:00
parent 5531e52352
commit f4a0a6a6fb
4 changed files with 166 additions and 171 deletions

View file

@ -120,7 +120,7 @@ jobs:
install -dm755 "${pkgdir}/usr/bin"
cat > "${pkgdir}/usr/bin/friendly-kobold" << 'WRAPPER'
#!/bin/bash
exec /opt/friendly-kobold/friendly-kobold "$@"
exec "/opt/friendly-kobold/Friendly Kobold" "$@"
WRAPPER
chmod +x "${pkgdir}/usr/bin/friendly-kobold"
@ -140,8 +140,15 @@ jobs:
# Install icon
install -dm755 "${pkgdir}/usr/share/pixmaps"
# Try different possible icon locations
if [ -f "${pkgdir}/opt/friendly-kobold/resources/assets/icon.png" ]; then
cp "${pkgdir}/opt/friendly-kobold/resources/assets/icon.png" "${pkgdir}/usr/share/pixmaps/friendly-kobold.png"
elif [ -f "${pkgdir}/opt/friendly-kobold/assets/icon.png" ]; then
cp "${pkgdir}/opt/friendly-kobold/assets/icon.png" "${pkgdir}/usr/share/pixmaps/friendly-kobold.png"
elif [ -f "${pkgdir}/opt/friendly-kobold/icon.png" ]; then
cp "${pkgdir}/opt/friendly-kobold/icon.png" "${pkgdir}/usr/share/pixmaps/friendly-kobold.png"
else
echo "Warning: Could not find icon.png in expected locations"
fi
}
EOF

View file

@ -1,7 +1,7 @@
{
"name": "friendly-kobold",
"productName": "Friendly Kobold",
"version": "0.5.7",
"version": "0.5.8",
"description": "A modern desktop app for running Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
@ -88,6 +88,9 @@
"dependencies": {
"@mantine/core": "^8.2.7",
"@mantine/hooks": "^8.2.7",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"execa": "^9.6.0",
"got": "^14.4.7",
"lucide-react": "^0.541.0",

View file

@ -1,212 +1,169 @@
import { useState, useEffect, useRef } from 'react';
import {
Box,
ScrollArea,
Text,
ActionIcon,
useComputedColorScheme,
} from '@mantine/core';
import { ChevronDown } from 'lucide-react';
import styles from '@/styles/layout.module.css';
import { useEffect, useRef } from 'react';
import { Box, useComputedColorScheme } from '@mantine/core';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
import { UI } from '@/constants';
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 restOfData = newData.slice(i + 1);
const nextNewlinePos = restOfData.indexOf('\n');
const replacementText =
nextNewlinePos === -1
? restOfData
: restOfData.slice(0, nextNewlinePos);
lines[lines.length - 1] = replacementText;
result = lines.join('\n');
i += 1 + replacementText.length;
if (nextNewlinePos !== -1) {
result += '\n';
i += 1;
}
} else {
i++;
}
}
} else {
result += char;
i++;
}
}
return result;
} catch (error) {
window.electronAPI.logs.logError('Terminal CR Error', error as Error);
return prevContent + newData;
}
};
export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false,
});
const [terminalContent, setTerminalContent] = useState<string>('');
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
const lastScrollTop = useRef<number>(0);
const terminalRef = useRef<HTMLDivElement>(null);
const xtermRef = useRef<Terminal | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const isDark = computedColorScheme === 'dark';
const handleScroll = ({ y }: { y: number }) => {
if (!viewportRef.current) return;
useEffect(() => {
if (!terminalRef.current) return;
const { scrollHeight, clientHeight } = viewportRef.current;
const isAtBottomNow = y + clientHeight >= scrollHeight - 10;
const terminal = new Terminal({
theme: {
background: '#1a1b1e',
foreground: '#ffffff',
cursor: '#ffffff',
selectionBackground: '#264f78',
},
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 14,
lineHeight: 1.4,
cursorBlink: false,
disableStdin: true,
allowTransparency: false,
scrollback: 10000,
convertEol: true,
});
if (y < lastScrollTop.current) {
setIsUserScrolling(true);
setShouldAutoScroll(false);
} else if (isAtBottomNow) {
setIsUserScrolling(false);
setShouldAutoScroll(true);
}
const fitAddon = new FitAddon();
const webLinksAddon = new WebLinksAddon();
lastScrollTop.current = y;
};
terminal.loadAddon(fitAddon);
terminal.loadAddon(webLinksAddon);
terminal.open(terminalRef.current);
setTimeout(() => {
fitAddon.fit();
}, 100);
xtermRef.current = terminal;
fitAddonRef.current = fitAddon;
terminal.writeln('\x1b[90mStarting KoboldCpp...\x1b[0m');
return () => {
terminal.dispose();
xtermRef.current = null;
fitAddonRef.current = null;
};
}, []);
useEffect(() => {
if (shouldAutoScroll && !isUserScrolling && viewportRef.current) {
const viewport = viewportRef.current;
viewport.scrollTop = viewport.scrollHeight;
if (xtermRef.current) {
const newTheme = {
background: isDark ? '#1a1b1e' : '#ffffff',
foreground: isDark ? '#ffffff' : '#000000',
cursor: isDark ? '#ffffff' : '#000000',
selectionBackground: isDark ? '#264f78' : '#0078d4',
};
xtermRef.current.options.theme = newTheme;
const element = xtermRef.current.element;
if (element) {
element.style.backgroundColor = newTheme.background;
element.style.color = newTheme.foreground;
}
xtermRef.current.refresh(0, xtermRef.current.rows - 1);
}
}, [terminalContent, shouldAutoScroll, isUserScrolling]);
}, [isDark]);
useEffect(() => {
const handleResize = () => {
if (fitAddonRef.current && xtermRef.current) {
setTimeout(() => {
fitAddonRef.current?.fit();
}, 50);
}
};
const resizeObserver = new ResizeObserver(handleResize);
if (terminalRef.current) {
resizeObserver.observe(terminalRef.current);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
resizeObserver.disconnect();
};
}, []);
useEffect(() => {
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
setTerminalContent((prev) => {
const newData = data.toString();
if (!xtermRef.current) return;
if (
onServerReady &&
newData.includes('Please connect to custom endpoint at ')
) {
const match = newData.match(
/Please connect to custom endpoint at (http:\/\/[^\s]+)/
);
if (match) {
const serverUrl = match[1];
setTimeout(() => onServerReady(serverUrl), 1500);
}
const newData = data.toString();
if (
onServerReady &&
newData.includes('Please connect to custom endpoint at ')
) {
const match = newData.match(
/Please connect to custom endpoint at (http:\/\/[^\s]+)/
);
if (match) {
const serverUrl = match[1];
setTimeout(() => onServerReady(serverUrl), 1500);
}
}
return handleCarriageReturns(prev, newData);
});
xtermRef.current.write(newData);
setTimeout(() => {
if (xtermRef.current) {
xtermRef.current.scrollToBottom();
}
}, 0);
});
return cleanup;
}, [onServerReady]);
const scrollToBottom = () => {
if (viewportRef.current) {
const viewport = viewportRef.current;
viewport.scrollTop = viewport.scrollHeight;
setShouldAutoScroll(true);
setIsUserScrolling(false);
}
};
return (
<Box
style={{
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
display: 'flex',
flexDirection: 'column',
backgroundColor: isDark
? 'var(--mantine-color-dark-filled)'
: 'var(--mantine-color-gray-0)',
borderRadius: 'inherit',
position: 'relative',
padding: '0.5rem',
display: 'flex',
flexDirection: 'column',
}}
>
<ScrollArea
ref={scrollAreaRef}
viewportRef={viewportRef}
onScrollPositionChange={handleScroll}
className={styles.terminalScrollArea}
scrollbarSize={8}
offsetScrollbars={false}
>
<Box p="md">
{terminalContent.length === 0 ? (
<Text c="dimmed" style={{ fontFamily: 'inherit' }}>
Starting KoboldCpp...
</Text>
) : (
<div
style={{
margin: 0,
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: '14px',
lineHeight: 1.4,
color: isDark
? 'var(--mantine-color-gray-0)'
: 'var(--mantine-color-dark-filled)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{terminalContent}
</div>
)}
</Box>
</ScrollArea>
{isUserScrolling && !shouldAutoScroll && (
<ActionIcon
variant="filled"
color="blue"
size="lg"
radius="xl"
onClick={scrollToBottom}
style={{
position: 'absolute',
bottom: '20px',
right: '20px',
zIndex: 10,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
}}
aria-label="Scroll to bottom"
>
<ChevronDown size={20} />
</ActionIcon>
)}
<div
ref={terminalRef}
style={{
height: '100%',
width: '100%',
flex: 1,
minHeight: 0,
backgroundColor: isDark ? '#1a1b1e' : '#ffffff',
borderRadius: '0.25rem',
overflow: 'hidden',
}}
/>
</Box>
);
};

View file

@ -2106,6 +2106,31 @@ __metadata:
languageName: node
linkType: hard
"@xterm/addon-fit@npm:^0.10.0":
version: 0.10.0
resolution: "@xterm/addon-fit@npm:0.10.0"
peerDependencies:
"@xterm/xterm": ^5.0.0
checksum: 10c0/76926120fc940376afef2cb68b15aec2a99fc628b6e3cc84f2bcb1682ca9b87f982b3c10ff206faf4ebc5b410467b81a7b5e83be37b4ac386586f472e4fa1c61
languageName: node
linkType: hard
"@xterm/addon-web-links@npm:^0.11.0":
version: 0.11.0
resolution: "@xterm/addon-web-links@npm:0.11.0"
peerDependencies:
"@xterm/xterm": ^5.0.0
checksum: 10c0/9426bed80afa954b0ea97771d041eb44e77a64e560ce8b8ef507a5d3a763979af18ae9f74ed54007bb7e235d0daf035be2a33f90d8edfecb431caf8ba0b0664e
languageName: node
linkType: hard
"@xterm/xterm@npm:^5.5.0":
version: 5.5.0
resolution: "@xterm/xterm@npm:5.5.0"
checksum: 10c0/358801feece58617d777b2783bec68dac1f52f736da3b0317f71a34f4e25431fb0b1920244f678b8d673f797145b4858c2a5ccb463a4a6df7c10c9093f1c9267
languageName: node
linkType: hard
"abbrev@npm:^1.0.0":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
@ -4391,6 +4416,9 @@ __metadata:
"@typescript-eslint/eslint-plugin": "npm:^8.40.0"
"@typescript-eslint/parser": "npm:^8.40.0"
"@vitejs/plugin-react": "npm:^5.0.1"
"@xterm/addon-fit": "npm:^0.10.0"
"@xterm/addon-web-links": "npm:^0.11.0"
"@xterm/xterm": "npm:^5.5.0"
cross-env: "npm:^10.0.0"
cspell: "npm:^9.2.0"
electron: "npm:^37.3.1"