feat (ui): File picker for scheduling recipes default to recipe dir (#3611)

This commit is contained in:
Angela Ning
2025-07-28 15:39:15 -04:00
committed by GitHub
parent 959f51ffc4
commit 7e98329093
6 changed files with 54 additions and 22 deletions
@@ -6,6 +6,7 @@ import { Select } from '../ui/Select';
import cronstrue from 'cronstrue';
import * as yaml from 'yaml';
import { Recipe, decodeRecipe } from '../../recipe';
import { getStorageDirectory } from '../../recipe/recipeStorage';
import ClockIcon from '../../assets/clock-icon.svg';
type FrequencyValue = 'once' | 'every' | 'daily' | 'weekly' | 'monthly';
@@ -361,7 +362,9 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
};
const handleBrowseFile = async () => {
const filePath = await window.electron.selectFileOrDirectory();
// Default to global recipes directory, but fallback to local if needed
const defaultPath = getStorageDirectory(true);
const filePath = await window.electron.selectFileOrDirectory(defaultPath);
if (filePath) {
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
setRecipeSourcePath(filePath);
+1 -1
View File
@@ -3,7 +3,7 @@ import { createServer } from 'net';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { getBinaryPath } from './utils/binaryPath';
import { getBinaryPath } from './utils/pathUtils';
import log from './utils/logger';
import { App } from 'electron';
import { Buffer } from 'node:buffer';
+32 -17
View File
@@ -1,4 +1,4 @@
import type { OpenDialogReturnValue } from 'electron';
import type { OpenDialogReturnValue, OpenDialogOptions } from 'electron';
import {
app,
App,
@@ -20,10 +20,11 @@ import fs from 'node:fs/promises';
import fsSync from 'node:fs';
import started from 'electron-squirrel-startup';
import path from 'node:path';
import os from 'node:os';
import { spawn } from 'child_process';
import 'dotenv/config';
import { startGoosed } from './goosed';
import { getBinaryPath } from './utils/binaryPath';
import { getBinaryPath, expandTilde } from './utils/pathUtils';
import { loadShellEnv } from './utils/loadEnv';
import log from './utils/logger';
import { ensureWinShims } from './utils/winShims';
@@ -1176,10 +1177,32 @@ ipcMain.handle('get-wakelock-state', () => {
});
// Add file/directory selection handler
ipcMain.handle('select-file-or-directory', async () => {
const result = (await dialog.showOpenDialog({
ipcMain.handle('select-file-or-directory', async (_event, defaultPath?: string) => {
const dialogOptions: OpenDialogOptions = {
properties: process.platform === 'darwin' ? ['openFile', 'openDirectory'] : ['openFile'],
})) as unknown as OpenDialogReturnValue;
};
// Set default path if provided
if (defaultPath) {
// Expand tilde to home directory
const expandedPath = expandTilde(defaultPath);
// Check if the path exists
try {
const stats = await fs.stat(expandedPath);
if (stats.isDirectory()) {
dialogOptions.defaultPath = expandedPath;
} else {
dialogOptions.defaultPath = path.dirname(expandedPath);
}
} catch (error) {
// If path doesn't exist, fall back to home directory and log error
console.error(`Default path does not exist: ${expandedPath}, falling back to home directory`);
dialogOptions.defaultPath = os.homedir();
}
}
const result = (await dialog.showOpenDialog(dialogOptions)) as unknown as OpenDialogReturnValue;
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
@@ -1449,9 +1472,7 @@ ipcMain.handle('get-binary-path', (_event, binaryName) => {
ipcMain.handle('read-file', (_event, filePath) => {
return new Promise((resolve) => {
// Expand tilde to home directory
const expandedPath = filePath.startsWith('~')
? path.join(app.getPath('home'), filePath.slice(1))
: filePath;
const expandedPath = expandTilde(filePath);
const cat = spawn('cat', [expandedPath]);
let output = '';
@@ -1484,9 +1505,7 @@ ipcMain.handle('read-file', (_event, filePath) => {
ipcMain.handle('write-file', (_event, filePath, content) => {
return new Promise((resolve) => {
// Expand tilde to home directory
const expandedPath = filePath.startsWith('~')
? path.join(app.getPath('home'), filePath.slice(1))
: filePath;
const expandedPath = expandTilde(filePath);
// Create a write stream to the file
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -1505,9 +1524,7 @@ ipcMain.handle('write-file', (_event, filePath, content) => {
ipcMain.handle('ensure-directory', async (_event, dirPath) => {
try {
// Expand tilde to home directory
const expandedPath = dirPath.startsWith('~')
? path.join(app.getPath('home'), dirPath.slice(1))
: dirPath;
const expandedPath = expandTilde(dirPath);
await fs.mkdir(expandedPath, { recursive: true });
return true;
@@ -1520,9 +1537,7 @@ ipcMain.handle('ensure-directory', async (_event, dirPath) => {
ipcMain.handle('list-files', async (_event, dirPath, extension) => {
try {
// Expand tilde to home directory
const expandedPath = dirPath.startsWith('~')
? path.join(app.getPath('home'), dirPath.slice(1))
: dirPath;
const expandedPath = expandTilde(dirPath);
const files = await fs.readdir(expandedPath);
if (extension) {
+3 -2
View File
@@ -62,7 +62,7 @@ type ElectronAPI = {
fetchMetadata: (url: string) => Promise<string>;
reloadApp: () => void;
checkForOllama: () => Promise<boolean>;
selectFileOrDirectory: () => Promise<string | null>;
selectFileOrDirectory: (defaultPath?: string) => Promise<string | null>;
startPowerSaveBlocker: () => Promise<number>;
stopPowerSaveBlocker: () => Promise<void>;
getBinaryPath: (binaryName: string) => Promise<string>;
@@ -151,7 +151,8 @@ const electronAPI: ElectronAPI = {
fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url),
reloadApp: () => ipcRenderer.send('reload-app'),
checkForOllama: () => ipcRenderer.invoke('check-ollama'),
selectFileOrDirectory: () => ipcRenderer.invoke('select-file-or-directory'),
selectFileOrDirectory: (defaultPath?: string) =>
ipcRenderer.invoke('select-file-or-directory', defaultPath),
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName),
+1 -1
View File
@@ -31,7 +31,7 @@ function parseLastModified(val: string | Date): Date {
/**
* Get the storage directory path for recipes
*/
function getStorageDirectory(isGlobal: boolean): string {
export function getStorageDirectory(isGlobal: boolean): string {
return isGlobal ? '~/.config/goose/recipes' : '.goose/recipes';
}
@@ -1,5 +1,6 @@
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import Electron from 'electron';
import log from './logger';
@@ -111,3 +112,15 @@ const addPaths = (
}
}
};
/**
* Expands tilde (~) to the user's home directory
* @param filePath - The file path that may contain tilde
* @returns The expanded path with tilde replaced by home directory
*/
export function expandTilde(filePath: string): string {
if (filePath.startsWith('~')) {
return path.join(os.homedir(), filePath.slice(1));
}
return filePath;
}