diff --git a/apps/code/src/main/platform-adapters/electron-storage-paths.ts b/apps/code/src/main/platform-adapters/electron-storage-paths.ts index 4bdbf639b2..e92e1e4d86 100644 --- a/apps/code/src/main/platform-adapters/electron-storage-paths.ts +++ b/apps/code/src/main/platform-adapters/electron-storage-paths.ts @@ -1,6 +1,8 @@ +import { dirname } from "node:path"; import type { IStoragePaths } from "@posthog/platform/storage-paths"; import { app } from "electron"; import { injectable } from "inversify"; +import { getLogFilePath } from "../utils/logger"; @injectable() export class ElectronStoragePaths implements IStoragePaths { @@ -11,4 +13,8 @@ export class ElectronStoragePaths implements IStoragePaths { public get logsPath(): string { return app.getPath("logs"); } + + public get logFolderPath(): string { + return dirname(getLogFilePath()); + } } diff --git a/packages/host-router/src/routers/os.router.ts b/packages/host-router/src/routers/os.router.ts index d4d541d158..77cd41f094 100644 --- a/packages/host-router/src/routers/os.router.ts +++ b/packages/host-router/src/routers/os.router.ts @@ -59,6 +59,10 @@ export const osRouter = router({ ctx.container.get(OS_SERVICE).openExternal(input.url), ), + showLogFolder: publicProcedure.mutation(({ ctx }) => + ctx.container.get(OS_SERVICE).showLogFolder(), + ), + searchDirectories: publicProcedure .input(searchDirectoriesInput) .query(({ ctx, input }) => diff --git a/packages/platform/src/storage-paths.ts b/packages/platform/src/storage-paths.ts index 23e6c9340d..b877c50301 100644 --- a/packages/platform/src/storage-paths.ts +++ b/packages/platform/src/storage-paths.ts @@ -1,6 +1,7 @@ export interface IStoragePaths { readonly appDataPath: string; readonly logsPath: string; + readonly logFolderPath: string; } export const STORAGE_PATHS_SERVICE = Symbol.for( diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index eddc6c2093..47d81f2ddc 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -50,7 +50,9 @@ export type CommandMenuAction = | "go-back" | "go-forward" | "open-task" - | "open-channel"; + | "open-channel" + | "reload-window" + | "show-log-folder"; // Event property interfaces export interface TaskListViewProperties { diff --git a/packages/ui/src/features/command/CommandMenu.tsx b/packages/ui/src/features/command/CommandMenu.tsx index c1fa5b9aba..43a7cf7e26 100644 --- a/packages/ui/src/features/command/CommandMenu.tsx +++ b/packages/ui/src/features/command/CommandMenu.tsx @@ -44,6 +44,7 @@ import { import { useAppView } from "@posthog/ui/router/useAppView"; import { openTask, openTaskInput } from "@posthog/ui/router/useOpenTask"; import { track } from "@posthog/ui/shell/analytics"; +import { showLogFolder } from "@posthog/ui/shell/openExternal"; import { useThemeStore } from "@posthog/ui/shell/themeStore"; import { DesktopIcon, @@ -51,6 +52,7 @@ import { GearIcon, HomeIcon, MoonIcon, + ReloadIcon, SunIcon, ViewVerticalIcon, } from "@radix-ui/react-icons"; @@ -280,9 +282,30 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { }, ]; + const developer: Command[] = [ + { + id: "show-log-folder", + label: "Show log folder", + keywords: "logs debug files finder", + icon: , + action: "show-log-folder", + onRun: showLogFolder, + }, + { + id: "reload-window", + label: "Reload window", + keywords: "refresh restart", + icon: , + action: "reload-window", + shortcut: SHORTCUTS.RELOAD_WINDOW, + onRun: () => window.location.reload(), + }, + ]; + const out: CommandSection[] = [ { label: "Actions", items: actions }, { label: "Navigation", items: navigation }, + { label: "Developer", items: developer }, ]; if (folders.length > 0) { diff --git a/packages/ui/src/features/command/keyboard-shortcuts.ts b/packages/ui/src/features/command/keyboard-shortcuts.ts index adc2bc2c19..87ecb99e35 100644 --- a/packages/ui/src/features/command/keyboard-shortcuts.ts +++ b/packages/ui/src/features/command/keyboard-shortcuts.ts @@ -25,6 +25,7 @@ export const SHORTCUTS = { BLUR: "escape", SUBMIT_BLUR: "mod+enter", SWITCH_MESSAGING_MODE: "mod+s", + RELOAD_WINDOW: "mod+shift+r", } as const; export type ShortcutCategory = "general" | "navigation" | "panels" | "editor"; diff --git a/packages/ui/src/shell/GlobalEventHandlers.tsx b/packages/ui/src/shell/GlobalEventHandlers.tsx index 7c1552752c..4664c87964 100644 --- a/packages/ui/src/shell/GlobalEventHandlers.tsx +++ b/packages/ui/src/shell/GlobalEventHandlers.tsx @@ -167,6 +167,11 @@ export function GlobalEventHandlers({ setReviewMode(currentTaskId, mode === "closed" ? "split" : "closed"); }, [currentTaskId, getReviewMode, setReviewMode]); + useHotkeys( + SHORTCUTS.RELOAD_WINDOW, + () => window.location.reload(), + globalOptions, + ); useHotkeys(SHORTCUTS.TOGGLE_LEFT_SIDEBAR, toggleLeftSidebar, globalOptions); useHotkeys(SHORTCUTS.TOGGLE_REVIEW_PANEL, handleToggleReview, globalOptions); useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions); diff --git a/packages/ui/src/shell/openExternal.ts b/packages/ui/src/shell/openExternal.ts index c566ed861b..3d6613a45c 100644 --- a/packages/ui/src/shell/openExternal.ts +++ b/packages/ui/src/shell/openExternal.ts @@ -9,3 +9,9 @@ export function openExternalUrl(url: string): void { url, }); } + +export function showLogFolder(): void { + void resolveService( + HOST_TRPC_CLIENT, + ).os.showLogFolder.mutate(); +} diff --git a/packages/workspace-server/src/services/os/os.test.ts b/packages/workspace-server/src/services/os/os.test.ts index 88ced62bce..dc0b0ce676 100644 --- a/packages/workspace-server/src/services/os/os.test.ts +++ b/packages/workspace-server/src/services/os/os.test.ts @@ -32,12 +32,19 @@ function createService() { getWorktreeLocation: vi.fn(() => "/tmp/worktrees"), }; + const storagePaths = { + appDataPath: "/data", + logsPath: "/logs", + logFolderPath: "/logs", + }; + const service = new OsService( dialog as never, urlLauncher as never, appMeta as never, imageProcessor as never, workspaceSettings as never, + storagePaths as never, ); return { service, dialog, urlLauncher, appMeta, workspaceSettings }; @@ -152,6 +159,14 @@ describe("OsService simple delegations", () => { await service.openExternal("https://posthog.com"); expect(urlLauncher.launch).toHaveBeenCalledWith("https://posthog.com"); }); + + it("opens the log folder as a file URL via the url launcher", async () => { + const { service, urlLauncher } = createService(); + await service.showLogFolder(); + expect(urlLauncher.launch).toHaveBeenCalledWith( + expect.stringMatching(/^file:\/\//), + ); + }); }); describe("OsService.getClaudePermissions", () => { diff --git a/packages/workspace-server/src/services/os/os.ts b/packages/workspace-server/src/services/os/os.ts index 73cc7cca14..08a3b1d065 100644 --- a/packages/workspace-server/src/services/os/os.ts +++ b/packages/workspace-server/src/services/os/os.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import { APP_META_SERVICE, type IAppMeta } from "@posthog/platform/app-meta"; import { DIALOG_SERVICE, @@ -11,6 +12,10 @@ import { type IImageProcessor, IMAGE_PROCESSOR_SERVICE, } from "@posthog/platform/image-processor"; +import { + type IStoragePaths, + STORAGE_PATHS_SERVICE, +} from "@posthog/platform/storage-paths"; import { type IUrlLauncher, URL_LAUNCHER_SERVICE, @@ -55,6 +60,8 @@ export class OsService { private readonly imageProcessor: IImageProcessor, @inject(WORKSPACE_SETTINGS_SERVICE) private readonly workspaceSettings: IWorkspaceSettings, + @inject(STORAGE_PATHS_SERVICE) + private readonly storagePaths: IStoragePaths, ) {} async getClaudePermissions(): Promise { @@ -162,6 +169,12 @@ export class OsService { await this.urlLauncher.launch(url); } + async showLogFolder(): Promise { + await this.urlLauncher.launch( + pathToFileURL(this.storagePaths.logFolderPath).href, + ); + } + async searchDirectories(query: string): Promise { if (!query?.trim()) return [];