From 94e209a8500039b3ae1855192913fd1e20b28392 Mon Sep 17 00:00:00 2001 From: Angela Ning Date: Wed, 23 Jul 2025 13:54:14 -0400 Subject: [PATCH 1/2] feat (ui): File picker for scheduling recipes should default to recipe dir --- .../schedule/CreateScheduleModal.tsx | 5 ++- ui/desktop/src/main.ts | 33 ++++++++++++++++--- ui/desktop/src/preload.ts | 5 +-- ui/desktop/src/recipe/recipeStorage.ts | 2 +- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/ui/desktop/src/components/schedule/CreateScheduleModal.tsx b/ui/desktop/src/components/schedule/CreateScheduleModal.tsx index e73d9dd21ae4..b0dd9c0e67cd 100644 --- a/ui/desktop/src/components/schedule/CreateScheduleModal.tsx +++ b/ui/desktop/src/components/schedule/CreateScheduleModal.tsx @@ -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 = ({ }; 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); diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 84ac0b526876..e546e53a6274 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -1,4 +1,4 @@ -import type { OpenDialogReturnValue } from 'electron'; +import type { OpenDialogReturnValue, OpenDialogOptions } from 'electron'; import { app, App, @@ -20,6 +20,7 @@ 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'; @@ -1177,10 +1178,34 @@ 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 = defaultPath.startsWith('~') + ? path.join(os.homedir(), defaultPath.slice(1)) + : 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]; diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index cd701b0d2638..6859aaabe10f 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -62,7 +62,7 @@ type ElectronAPI = { fetchMetadata: (url: string) => Promise; reloadApp: () => void; checkForOllama: () => Promise; - selectFileOrDirectory: () => Promise; + selectFileOrDirectory: (defaultPath?: string) => Promise; startPowerSaveBlocker: () => Promise; stopPowerSaveBlocker: () => Promise; getBinaryPath: (binaryName: string) => Promise; @@ -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), diff --git a/ui/desktop/src/recipe/recipeStorage.ts b/ui/desktop/src/recipe/recipeStorage.ts index 6941598d5643..3ab549133925 100644 --- a/ui/desktop/src/recipe/recipeStorage.ts +++ b/ui/desktop/src/recipe/recipeStorage.ts @@ -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'; } From 360c6dbf8d980e9b126dac9f7b740f0219c9344f Mon Sep 17 00:00:00 2001 From: Angela Ning Date: Mon, 28 Jul 2025 15:03:30 -0400 Subject: [PATCH 2/2] refactor and reuse helper function for expanding ~ to home dir --- ui/desktop/src/goosed.ts | 2 +- ui/desktop/src/main.ts | 22 +++++-------------- .../src/utils/{binaryPath.ts => pathUtils.ts} | 13 +++++++++++ 3 files changed, 20 insertions(+), 17 deletions(-) rename ui/desktop/src/utils/{binaryPath.ts => pathUtils.ts} (90%) diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 802df11a93d5..a43bd4df84d7 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -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'; diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index e546e53a6274..778fd41a0c55 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -24,7 +24,7 @@ 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'; @@ -1186,9 +1186,7 @@ ipcMain.handle('select-file-or-directory', async (_event, defaultPath?: string) // Set default path if provided if (defaultPath) { // Expand tilde to home directory - const expandedPath = defaultPath.startsWith('~') - ? path.join(os.homedir(), defaultPath.slice(1)) - : defaultPath; + const expandedPath = expandTilde(defaultPath); // Check if the path exists try { @@ -1475,9 +1473,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 = ''; @@ -1510,9 +1506,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 @@ -1531,9 +1525,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; @@ -1546,9 +1538,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) { diff --git a/ui/desktop/src/utils/binaryPath.ts b/ui/desktop/src/utils/pathUtils.ts similarity index 90% rename from ui/desktop/src/utils/binaryPath.ts rename to ui/desktop/src/utils/pathUtils.ts index e7de43426426..d13e91cd122a 100644 --- a/ui/desktop/src/utils/binaryPath.ts +++ b/ui/desktop/src/utils/pathUtils.ts @@ -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; +}