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 b174c15a1f..47d81f2ddc 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -47,8 +47,12 @@ export type CommandMenuAction = | "toggle-theme" | "toggle-left-sidebar" | "open-review-panel" + | "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 f0b33b6040..43a7cf7e26 100644 --- a/packages/ui/src/features/command/CommandMenu.tsx +++ b/packages/ui/src/features/command/CommandMenu.tsx @@ -1,4 +1,4 @@ -import { HashIcon } from "@phosphor-icons/react"; +import { CaretLeftIcon, CaretRightIcon, HashIcon } from "@phosphor-icons/react"; import { Autocomplete, AutocompleteCollection, @@ -10,6 +10,7 @@ import { AutocompleteStatus, Dialog, DialogContent, + Kbd, } from "@posthog/quill"; import { PROJECT_BLUEBIRD_FLAG } from "@posthog/shared"; import { @@ -21,6 +22,10 @@ import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useTaskChannelMap } from "@posthog/ui/features/canvas/hooks/useTaskChannelMap"; import { useReviewNavigationStore } from "@posthog/ui/features/code-review/reviewNavigationStore"; import { CommandKeyHints } from "@posthog/ui/features/command/CommandKeyHints"; +import { + formatHotkeyParts, + SHORTCUTS, +} from "@posthog/ui/features/command/keyboard-shortcuts"; import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; import { useFolders } from "@posthog/ui/features/folders/useFolders"; import { @@ -31,10 +36,15 @@ import { TaskIcon } from "@posthog/ui/features/sidebar/components/items/TaskIcon import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; import { useTaskPrStatus } from "@posthog/ui/features/sidebar/useTaskPrStatus"; import { useTasks } from "@posthog/ui/features/tasks/useTasks"; -import { navigateToChannel } from "@posthog/ui/router/navigationBridge"; +import { + goBackInHistory, + goForwardInHistory, + navigateToChannel, +} from "@posthog/ui/router/navigationBridge"; 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, @@ -42,6 +52,7 @@ import { GearIcon, HomeIcon, MoonIcon, + ReloadIcon, SunIcon, ViewVerticalIcon, } from "@radix-ui/react-icons"; @@ -62,6 +73,8 @@ type Command = { action: CommandMenuAction; /** Channel in scope for the bluebird open-channel / open-task actions. */ channelId?: string; + /** Hotkey string (e.g. "mod+b") shown right-aligned when present. */ + shortcut?: string; onRun: () => void; }; @@ -208,8 +221,27 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { label: "Settings", icon: , action: "settings", + shortcut: SHORTCUTS.SETTINGS, onRun: () => openSettingsDialog(), }, + { + id: "go-back", + label: "Go back", + keywords: "navigate history previous", + icon: , + action: "go-back", + shortcut: SHORTCUTS.GO_BACK, + onRun: goBackInHistory, + }, + { + id: "go-forward", + label: "Go forward", + keywords: "navigate history next", + icon: , + action: "go-forward", + shortcut: SHORTCUTS.GO_FORWARD, + onRun: goForwardInHistory, + }, ]; const actions: Command[] = [ @@ -219,6 +251,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { label: "Toggle left sidebar", icon: , action: "toggle-left-sidebar", + shortcut: SHORTCUTS.TOGGLE_LEFT_SIDEBAR, onRun: toggleLeftSidebar, }, ...(reviewTaskId @@ -230,6 +263,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { ), action: "open-review-panel" as CommandMenuAction, + shortcut: SHORTCUTS.TOGGLE_REVIEW_PANEL, onRun: openReviewPanel, }, ] @@ -240,6 +274,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { keywords: "create", icon: , action: "new-task", + shortcut: SHORTCUTS.NEW_TASK, onRun: () => { closeSettingsDialog(); openTaskInput(); @@ -247,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: "Navigation", items: navigation }, { label: "Actions", items: actions }, + { label: "Navigation", items: navigation }, + { label: "Developer", items: developer }, ]; if (folders.length > 0) { @@ -409,8 +465,12 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { value={cmd.id} onClick={() => handleSelect(cmd.id)} // Long task names wrap instead of truncating, so the - // item must grow: min-height, not a fixed height. - className="h-auto! min-h-7 py-1.5 text-left" + // item must grow: min-height, not a fixed height. Quill + // wraps our children in an inner content span; force it to + // fill the row (so a trailing shortcut can `ml-auto` to the + // end) and let it overflow visibly so the shortcut Kbd + // boxes aren't clipped by the wrapper's `truncate`. + className="flex h-auto! min-h-7 w-full items-center gap-2 py-1.5 pr-2 text-left [&>span]:w-full [&>span]:overflow-visible" > {cmd.icon} @@ -421,6 +481,13 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { ยท #{cmd.detail} )} + {cmd.shortcut && ( + + {formatHotkeyParts(cmd.shortcut).map((part) => ( + {part} + ))} + + )} )} 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 [];