mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
back to xterm, correct AUR install path
This commit is contained in:
parent
5531e52352
commit
f4a0a6a6fb
4 changed files with 166 additions and 171 deletions
9
.github/workflows/aur-release.yml
vendored
9
.github/workflows/aur-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
28
yarn.lock
28
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue