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" install -dm755 "${pkgdir}/usr/bin"
cat > "${pkgdir}/usr/bin/friendly-kobold" << 'WRAPPER' cat > "${pkgdir}/usr/bin/friendly-kobold" << 'WRAPPER'
#!/bin/bash #!/bin/bash
exec /opt/friendly-kobold/friendly-kobold "$@" exec "/opt/friendly-kobold/Friendly Kobold" "$@"
WRAPPER WRAPPER
chmod +x "${pkgdir}/usr/bin/friendly-kobold" chmod +x "${pkgdir}/usr/bin/friendly-kobold"
@ -140,8 +140,15 @@ jobs:
# Install icon # Install icon
install -dm755 "${pkgdir}/usr/share/pixmaps" install -dm755 "${pkgdir}/usr/share/pixmaps"
# Try different possible icon locations
if [ -f "${pkgdir}/opt/friendly-kobold/resources/assets/icon.png" ]; then 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" 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 fi
} }
EOF EOF

View file

@ -1,7 +1,7 @@
{ {
"name": "friendly-kobold", "name": "friendly-kobold",
"productName": "Friendly Kobold", "productName": "Friendly Kobold",
"version": "0.5.7", "version": "0.5.8",
"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": "./",
@ -88,6 +88,9 @@
"dependencies": { "dependencies": {
"@mantine/core": "^8.2.7", "@mantine/core": "^8.2.7",
"@mantine/hooks": "^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", "execa": "^9.6.0",
"got": "^14.4.7", "got": "^14.4.7",
"lucide-react": "^0.541.0", "lucide-react": "^0.541.0",

View file

@ -1,115 +1,117 @@
import { useState, useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { import { Box, useComputedColorScheme } from '@mantine/core';
Box, import { Terminal } from '@xterm/xterm';
ScrollArea, import { FitAddon } from '@xterm/addon-fit';
Text, import { WebLinksAddon } from '@xterm/addon-web-links';
ActionIcon, import '@xterm/xterm/css/xterm.css';
useComputedColorScheme,
} from '@mantine/core';
import { ChevronDown } from 'lucide-react';
import styles from '@/styles/layout.module.css';
import { UI } from '@/constants'; import { UI } from '@/constants';
interface TerminalTabProps { 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 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) => { export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
const computedColorScheme = useComputedColorScheme('light', { const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false, getInitialValueInEffect: false,
}); });
const [terminalContent, setTerminalContent] = useState<string>(''); const terminalRef = useRef<HTMLDivElement>(null);
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false); const xtermRef = useRef<Terminal | null>(null);
const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true); const fitAddonRef = useRef<FitAddon | null>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
const lastScrollTop = useRef<number>(0);
const isDark = computedColorScheme === 'dark'; const isDark = computedColorScheme === 'dark';
const handleScroll = ({ y }: { y: number }) => { useEffect(() => {
if (!viewportRef.current) return; if (!terminalRef.current) return;
const { scrollHeight, clientHeight } = viewportRef.current; const terminal = new Terminal({
const isAtBottomNow = y + clientHeight >= scrollHeight - 10; 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) { const fitAddon = new FitAddon();
setIsUserScrolling(true); const webLinksAddon = new WebLinksAddon();
setShouldAutoScroll(false);
} else if (isAtBottomNow) {
setIsUserScrolling(false);
setShouldAutoScroll(true);
}
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(() => { useEffect(() => {
if (shouldAutoScroll && !isUserScrolling && viewportRef.current) { if (xtermRef.current) {
const viewport = viewportRef.current; const newTheme = {
viewport.scrollTop = viewport.scrollHeight; 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;
} }
}, [terminalContent, shouldAutoScroll, isUserScrolling]);
xtermRef.current.refresh(0, xtermRef.current.rows - 1);
}
}, [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(() => { useEffect(() => {
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => { const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
setTerminalContent((prev) => { if (!xtermRef.current) return;
const newData = data.toString(); const newData = data.toString();
if ( if (
@ -125,88 +127,43 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
} }
} }
return handleCarriageReturns(prev, newData); xtermRef.current.write(newData);
});
setTimeout(() => {
if (xtermRef.current) {
xtermRef.current.scrollToBottom();
}
}, 0);
}); });
return cleanup; return cleanup;
}, [onServerReady]); }, [onServerReady]);
const scrollToBottom = () => {
if (viewportRef.current) {
const viewport = viewportRef.current;
viewport.scrollTop = viewport.scrollHeight;
setShouldAutoScroll(true);
setIsUserScrolling(false);
}
};
return ( return (
<Box <Box
style={{ style={{
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`, height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
display: 'flex',
flexDirection: 'column',
backgroundColor: isDark backgroundColor: isDark
? 'var(--mantine-color-dark-filled)' ? 'var(--mantine-color-dark-filled)'
: 'var(--mantine-color-gray-0)', : 'var(--mantine-color-gray-0)',
borderRadius: 'inherit', 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 <div
ref={terminalRef}
style={{ style={{
margin: 0, height: '100%',
fontFamily: width: '100%',
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', flex: 1,
fontSize: '14px', minHeight: 0,
lineHeight: 1.4, backgroundColor: isDark ? '#1a1b1e' : '#ffffff',
color: isDark borderRadius: '0.25rem',
? 'var(--mantine-color-gray-0)' overflow: 'hidden',
: '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>
)}
</Box> </Box>
); );
}; };

View file

@ -2106,6 +2106,31 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "abbrev@npm:^1.0.0":
version: 1.1.1 version: 1.1.1
resolution: "abbrev@npm:1.1.1" resolution: "abbrev@npm:1.1.1"
@ -4391,6 +4416,9 @@ __metadata:
"@typescript-eslint/eslint-plugin": "npm:^8.40.0" "@typescript-eslint/eslint-plugin": "npm:^8.40.0"
"@typescript-eslint/parser": "npm:^8.40.0" "@typescript-eslint/parser": "npm:^8.40.0"
"@vitejs/plugin-react": "npm:^5.0.1" "@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" cross-env: "npm:^10.0.0"
cspell: "npm:^9.2.0" cspell: "npm:^9.2.0"
electron: "npm:^37.3.1" electron: "npm:^37.3.1"