feat(desktop-ui): View and edit .goosehints file (#1431)
This commit is contained in:
@@ -5,6 +5,7 @@ import { Sliders } from 'lucide-react';
|
|||||||
import { ModelRadioList } from './settings/models/ModelRadioList';
|
import { ModelRadioList } from './settings/models/ModelRadioList';
|
||||||
import { Document, ChevronUp, ChevronDown } from './icons';
|
import { Document, ChevronUp, ChevronDown } from './icons';
|
||||||
import type { View } from '../ChatWindow';
|
import type { View } from '../ChatWindow';
|
||||||
|
import { ConfigureGooseHints } from './ConfigureGooseHints';
|
||||||
|
|
||||||
export default function BottomMenu({
|
export default function BottomMenu({
|
||||||
hasMessages,
|
hasMessages,
|
||||||
@@ -76,6 +77,10 @@ export default function BottomMenu({
|
|||||||
<ChevronUp className="ml-1" />
|
<ChevronUp className="ml-1" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div className="ml-4">
|
||||||
|
<ConfigureGooseHints directory={window.appConfig.get('GOOSE_WORKING_DIR')} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Model Selector Dropdown - Only in development */}
|
{/* Model Selector Dropdown - Only in development */}
|
||||||
<div className="relative flex items-center ml-auto mr-4" ref={dropdownRef}>
|
<div className="relative flex items-center ml-auto mr-4" ref={dropdownRef}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Card } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Check } from './icons';
|
||||||
|
|
||||||
|
const Modal = ({ children }) => (
|
||||||
|
<div className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards]">
|
||||||
|
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80%] h-[80%] bg-bgApp rounded-xl overflow-hidden shadow-none px-8 pt-[24px] pb-0">
|
||||||
|
<div className="flex flex-col space-y-8 text-base text-textStandard h-full">{children}</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalHeader = () => (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex">
|
||||||
|
<h2 className="text-2xl font-regular text-textStandard">Configure .goosehints</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalHelpText = () => (
|
||||||
|
<div className="text-sm flex-col space-y-4">
|
||||||
|
<p>
|
||||||
|
.goosehints is a text file used to provide additional context about your project and improve
|
||||||
|
the communication with Goose.
|
||||||
|
</p>
|
||||||
|
<p>You'll need to restart your session for .goosehints updates to take effect.</p>
|
||||||
|
<p>
|
||||||
|
See{' '}
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="text-blue-500 hover:text-blue-600 p-0 h-auto"
|
||||||
|
onClick={() =>
|
||||||
|
window.open('https://block.github.io/goose/docs/guides/using-goosehints/', '_blank')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
using .goosehints
|
||||||
|
</Button>{' '}
|
||||||
|
for more information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalError = ({ error }) => (
|
||||||
|
<div className="text-sm text-textSubtle">
|
||||||
|
<div className="text-red-600">Error reading .goosehints file: {JSON.stringify(error)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalFileInfo = ({ filePath, found }) => (
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{found ? (
|
||||||
|
<div className="text-green-600">
|
||||||
|
<Check className="w-4 h-4 inline-block" /> .goosehints file found at: {filePath}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>Creating new .goosehints file at: {filePath}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalButtons = ({ onSubmit, onCancel }) => (
|
||||||
|
<div className="-ml-8 -mr-8">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onSubmit}
|
||||||
|
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-base hover:bg-bgSubtle text-textProminent font-regular"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onCancel}
|
||||||
|
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-base font-regular"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getGoosehintsFile = async (filePath) => await window.electron.readFile(filePath);
|
||||||
|
|
||||||
|
type GoosehintsModalProps = {
|
||||||
|
directory: string;
|
||||||
|
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: GoosehintsModalProps) => {
|
||||||
|
const goosehintsFilePath = `${directory}/.goosehints`;
|
||||||
|
const [goosehintsFile, setGoosehintsFile] = useState<string>(null);
|
||||||
|
const [goosehintsFileFound, setGoosehintsFileFound] = useState<boolean>(false);
|
||||||
|
const [goosehintsFileReadError, setGoosehintsFileReadError] = useState<string>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchGoosehintsFile = async () => {
|
||||||
|
try {
|
||||||
|
const { file, error, found } = await getGoosehintsFile(goosehintsFilePath);
|
||||||
|
setGoosehintsFile(file);
|
||||||
|
setGoosehintsFileFound(found);
|
||||||
|
setGoosehintsFileReadError(error);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching .goosehints file:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (directory) fetchGoosehintsFile();
|
||||||
|
}, [directory, goosehintsFilePath]);
|
||||||
|
|
||||||
|
const writeFile = async () => {
|
||||||
|
await window.electron.writeFile(goosehintsFilePath, goosehintsFile);
|
||||||
|
setIsGoosehintsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
<ModalHeader />
|
||||||
|
<ModalHelpText />
|
||||||
|
<div className="flex-1">
|
||||||
|
{goosehintsFileReadError ? (
|
||||||
|
<ModalError error={goosehintsFileReadError} />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col space-y-2 h-full">
|
||||||
|
<ModalFileInfo filePath={goosehintsFilePath} found={goosehintsFileFound} />
|
||||||
|
<textarea
|
||||||
|
defaultValue={goosehintsFile}
|
||||||
|
autoFocus
|
||||||
|
className="w-full flex-1 border rounded-md min-h-40 p-2 text-sm resize-none"
|
||||||
|
onChange={(event) => setGoosehintsFile(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ModalButtons onSubmit={writeFile} onCancel={() => setIsGoosehintsModalOpen(false)} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigureGooseHints = ({ directory }: { directory: string }) => {
|
||||||
|
const [isGooseHintsModalOpen, setIsGoosehintsModalOpen] = useState<boolean>(false);
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer ml-4 hover:opacity-75"
|
||||||
|
onClick={() => setIsGoosehintsModalOpen(true)}
|
||||||
|
>
|
||||||
|
Configure .goosehints
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isGooseHintsModalOpen ? (
|
||||||
|
<GoosehintsModal
|
||||||
|
directory={directory}
|
||||||
|
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -623,6 +623,41 @@ app.whenReady().then(async () => {
|
|||||||
spawn('xdg-open', [url]);
|
spawn('xdg-open', [url]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('read-file', (event, filePath) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
exec(`cat ${filePath}`, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
// File not found
|
||||||
|
resolve({ file: "", filePath, error: null, found: false });
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
console.error('Error output:', stderr);
|
||||||
|
resolve({ file: "", filePath, error, found: false });
|
||||||
|
}
|
||||||
|
resolve({ file: stdout, filePath, error: null, found: true });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('write-file', (event, filePath, content) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const command = `cat << 'EOT' > ${filePath}
|
||||||
|
${content}
|
||||||
|
EOT`;
|
||||||
|
exec(command, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Error writing to file:', error);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
console.error('Error output:', stderr);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS.
|
// Quit when all windows are closed, except on macOS.
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ type ElectronAPI = {
|
|||||||
startPowerSaveBlocker: () => Promise<number>;
|
startPowerSaveBlocker: () => Promise<number>;
|
||||||
stopPowerSaveBlocker: () => Promise<void>;
|
stopPowerSaveBlocker: () => Promise<void>;
|
||||||
getBinaryPath: (binaryName: string) => Promise<string>;
|
getBinaryPath: (binaryName: string) => Promise<string>;
|
||||||
|
readFile: (directory: string) => Promise<{ file: string; filePath: string; error: string; found: boolean }>;
|
||||||
|
writeFile: (directory: string, content: string) => Promise<boolean>;
|
||||||
on: (
|
on: (
|
||||||
channel: string,
|
channel: string,
|
||||||
callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||||
@@ -50,6 +52,8 @@ const electronAPI: ElectronAPI = {
|
|||||||
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
|
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
|
||||||
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
||||||
getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName),
|
getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName),
|
||||||
|
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
|
||||||
|
writeFile: (filePath: string, content: string) => ipcRenderer.invoke('write-file', filePath, content),
|
||||||
on: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
on: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
||||||
ipcRenderer.on(channel, callback);
|
ipcRenderer.on(channel, callback);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user