From ad6ac943b58d6aa7c6012e1640b95e89b2066684 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 22 May 2026 15:54:13 -0300 Subject: [PATCH 1/8] feat(code): add Option+Space quick entry widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Claude-Desktop-style quick entry widget. Pressing Option+Space anywhere on macOS — even with PostHog Code minimized or in the background — pops a small floating bar at the bottom of the active display where the user can type a prompt and pick a recent folder. Submitting creates the task and routes the main window to the new task detail. Implementation: - New `QuickEntryService` orchestrates show/hide/submit and emits `FocusInput`/`Hide` events. Because services can't import `electron` directly, the BrowserWindow primitives live in `window.ts` (`createQuickEntryWindow`, `showQuickEntryWindow`, `hideQuickEntryWindow`, `destroyQuickEntryWindow`). - New `quickEntry` tRPC router (`toggle/show/hide/openTaskInMain/getRecentRepos/onFocusInput/onHide`). - New `OpenTask` event on `UIService` so the QE submit flow can tell the main renderer to navigate to a specific task. - Frameless, transparent, always-on-top BrowserWindow (720×132) loaded from the same renderer bundle with hash `#quick-entry`. Renderer entry branches on `window.location.hash` to mount `QuickEntryRoot` instead of `App`. - `QuickEntryView` reuses the existing `PromptInput` and calls `TaskService.createTask` with the user's last-used workspace mode, adapter, and model. - `globalShortcut.register("Alt+Space")` wired in `index.ts` with cleanup on `will-quit`. - Blur-to-hide with a 120ms grace period so dropdown popups don't dismiss the widget. Out of scope / follow-ups: settings UI toggle, customizable hotkey, true double-tap-Option detection (would add `uiohook-napi` and require macOS Accessibility permission), Windows/Linux polish. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- apps/code/src/main/di/container.ts | 2 + apps/code/src/main/di/tokens.ts | 1 + apps/code/src/main/index.ts | 33 ++- .../src/main/services/quick-entry/schemas.ts | 16 ++ .../src/main/services/quick-entry/service.ts | 121 +++++++++ apps/code/src/main/services/ui/schemas.ts | 2 + apps/code/src/main/services/ui/service.ts | 4 + apps/code/src/main/trpc/router.ts | 2 + .../code/src/main/trpc/routers/quick-entry.ts | 55 ++++ apps/code/src/main/trpc/routers/ui.ts | 1 + apps/code/src/main/window.ts | 155 ++++++++++- .../components/GlobalEventHandlers.tsx | 19 ++ .../features/quick-entry/QuickEntryRoot.tsx | 22 ++ .../features/quick-entry/QuickEntryView.tsx | 251 ++++++++++++++++++ apps/code/src/renderer/main.tsx | 57 ++-- 15 files changed, 713 insertions(+), 28 deletions(-) create mode 100644 apps/code/src/main/services/quick-entry/schemas.ts create mode 100644 apps/code/src/main/services/quick-entry/service.ts create mode 100644 apps/code/src/main/trpc/routers/quick-entry.ts create mode 100644 apps/code/src/renderer/features/quick-entry/QuickEntryRoot.tsx create mode 100644 apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx diff --git a/apps/code/src/main/di/container.ts b/apps/code/src/main/di/container.ts index b5b0d0461..e0edd5994 100644 --- a/apps/code/src/main/di/container.ts +++ b/apps/code/src/main/di/container.ts @@ -57,6 +57,7 @@ import { OAuthService } from "../services/oauth/service"; import { PosthogPluginService } from "../services/posthog-plugin/service"; import { ProcessTrackingService } from "../services/process-tracking/service"; import { ProvisioningService } from "../services/provisioning/service"; +import { QuickEntryService } from "../services/quick-entry/service"; import { settingsStore } from "../services/settingsStore"; import { ShellService } from "../services/shell/service"; import { SlackIntegrationService } from "../services/slack-integration/service"; @@ -148,5 +149,6 @@ container.bind(MAIN_TOKENS.TaskLinkService).to(TaskLinkService); container.bind(MAIN_TOKENS.InboxLinkService).to(InboxLinkService); container.bind(MAIN_TOKENS.WatcherRegistryService).to(WatcherRegistryService); container.bind(MAIN_TOKENS.WorkspaceService).to(WorkspaceService); +container.bind(MAIN_TOKENS.QuickEntryService).to(QuickEntryService); container.bind(MAIN_TOKENS.SettingsStore).toConstantValue(settingsStore); diff --git a/apps/code/src/main/di/tokens.ts b/apps/code/src/main/di/tokens.ts index 92d9a1287..4caa0f084 100644 --- a/apps/code/src/main/di/tokens.ts +++ b/apps/code/src/main/di/tokens.ts @@ -81,4 +81,5 @@ export const MAIN_TOKENS = Object.freeze({ ProvisioningService: Symbol.for("Main.ProvisioningService"), WorkspaceService: Symbol.for("Main.WorkspaceService"), EnrichmentService: Symbol.for("Main.EnrichmentService"), + QuickEntryService: Symbol.for("Main.QuickEntryService"), }); diff --git a/apps/code/src/main/index.ts b/apps/code/src/main/index.ts index b8a125493..874556a1d 100644 --- a/apps/code/src/main/index.ts +++ b/apps/code/src/main/index.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; import os from "node:os"; -import { app, BrowserWindow, dialog } from "electron"; +import { app, BrowserWindow, dialog, globalShortcut } from "electron"; import log from "electron-log/main"; import "./utils/logger"; import "./services/index.js"; @@ -24,6 +24,7 @@ import { trackAppEvent, } from "./services/posthog-analytics"; import type { PosthogPluginService } from "./services/posthog-plugin/service"; +import type { QuickEntryService } from "./services/quick-entry/service"; import type { SlackIntegrationService } from "./services/slack-integration/service"; import type { SuspensionService } from "./services/suspension/service"; import type { TaskLinkService } from "./services/task-link/service"; @@ -230,12 +231,42 @@ app.whenReady().then(async () => { createWindow(); await initializeServices(); initializeDeepLinks(); + initializeQuickEntry(); }); +function initializeQuickEntry(): void { + try { + const service = container.get( + MAIN_TOKENS.QuickEntryService, + ); + service.createWindow(); + + const accelerator = "Alt+Space"; + const ok = globalShortcut.register(accelerator, () => { + try { + service.toggle(); + } catch (err) { + log.error("Quick entry toggle failed", err); + } + }); + if (!ok) { + log.warn(`Failed to register global shortcut: ${accelerator}`); + } else { + log.info(`Registered quick-entry global shortcut: ${accelerator}`); + } + } catch (err) { + log.error("Failed to initialize quick entry", err); + } +} + app.on("window-all-closed", () => { app.quit(); }); +app.on("will-quit", () => { + globalShortcut.unregisterAll(); +}); + app.on("before-quit", async (event) => { let lifecycleService: AppLifecycleService; try { diff --git a/apps/code/src/main/services/quick-entry/schemas.ts b/apps/code/src/main/services/quick-entry/schemas.ts new file mode 100644 index 000000000..3dd40579c --- /dev/null +++ b/apps/code/src/main/services/quick-entry/schemas.ts @@ -0,0 +1,16 @@ +export const QuickEntryServiceEvent = { + FocusInput: "focus-input", + Hide: "hide", +} as const; + +export interface QuickEntryServiceEvents { + [QuickEntryServiceEvent.FocusInput]: true; + [QuickEntryServiceEvent.Hide]: true; +} + +export interface RecentRepoEntry { + id: string; + path: string; + name: string; + remoteUrl: string | null; +} diff --git a/apps/code/src/main/services/quick-entry/service.ts b/apps/code/src/main/services/quick-entry/service.ts new file mode 100644 index 000000000..38097c181 --- /dev/null +++ b/apps/code/src/main/services/quick-entry/service.ts @@ -0,0 +1,121 @@ +import { inject, injectable } from "inversify"; +import { MAIN_TOKENS } from "../../di/tokens"; +import { logger } from "../../utils/logger"; +import { TypedEventEmitter } from "../../utils/typed-event-emitter"; +import { + createQuickEntryWindow, + destroyQuickEntryWindow, + hideQuickEntryWindow, + isQuickEntryWindowFocused, + isQuickEntryWindowVisible, + showAndFocusMainWindow, + showQuickEntryWindow, +} from "../../window"; +import type { FoldersService } from "../folders/service"; +import type { UIService } from "../ui/service"; +import { + QuickEntryServiceEvent, + type QuickEntryServiceEvents, + type RecentRepoEntry, +} from "./schemas"; + +const log = logger.scope("quick-entry"); + +const BLUR_HIDE_GRACE_MS = 120; +const SHOW_GRACE_MS = 200; + +@injectable() +export class QuickEntryService extends TypedEventEmitter { + private suppressBlurHide = false; + private windowCreated = false; + + constructor( + @inject(MAIN_TOKENS.UIService) + private readonly uiService: UIService, + @inject(MAIN_TOKENS.FoldersService) + private readonly foldersService: FoldersService, + ) { + super(); + } + + createWindow(): void { + if (this.windowCreated) return; + createQuickEntryWindow({ + onBlur: () => this.handleBlur(), + }); + this.windowCreated = true; + } + + private handleBlur(): void { + if (this.suppressBlurHide) return; + // Child popups (dropdowns) briefly steal focus — grace period before hiding. + setTimeout(() => { + if (!isQuickEntryWindowVisible()) return; + if (isQuickEntryWindowFocused()) return; + this.hide(); + }, BLUR_HIDE_GRACE_MS); + } + + isVisible(): boolean { + return isQuickEntryWindowVisible(); + } + + toggle(): void { + if (this.isVisible()) { + this.hide(); + } else { + this.show(); + } + } + + show(): void { + this.suppressBlurHide = true; + const ok = showQuickEntryWindow(); + if (!ok) { + this.suppressBlurHide = false; + return; + } + this.emit(QuickEntryServiceEvent.FocusInput, true); + setTimeout(() => { + this.suppressBlurHide = false; + }, SHOW_GRACE_MS); + } + + hide(): void { + if (!isQuickEntryWindowVisible()) return; + hideQuickEntryWindow(); + this.emit(QuickEntryServiceEvent.Hide, true); + } + + openTaskInMain(taskId: string): void { + this.hide(); + showAndFocusMainWindow(); + if (taskId) { + this.uiService.openTask(taskId); + } + } + + async getRecentRepos(limit = 8): Promise { + const folders = await this.foldersService.getFolders(); + return folders + .filter((f) => f.exists) + .sort((a, b) => { + const ta = new Date(a.lastAccessed).getTime(); + const tb = new Date(b.lastAccessed).getTime(); + return tb - ta; + }) + .slice(0, limit) + .map((f) => ({ + id: f.id, + path: f.path, + name: f.name, + remoteUrl: f.remoteUrl, + })); + } + + dispose(): void { + destroyQuickEntryWindow(); + this.windowCreated = false; + log.info("Quick entry service disposed"); + } +} diff --git a/apps/code/src/main/services/ui/schemas.ts b/apps/code/src/main/services/ui/schemas.ts index ce77eecb9..d882e1505 100644 --- a/apps/code/src/main/services/ui/schemas.ts +++ b/apps/code/src/main/services/ui/schemas.ts @@ -5,6 +5,7 @@ export const UIServiceEvent = { ResetLayout: "reset-layout", ClearStorage: "clear-storage", InvalidateToken: "invalidate-token", + OpenTask: "open-task", } as const; // UI events are simple signals - payload is just a marker that the event fired @@ -14,4 +15,5 @@ export interface UIServiceEvents { [UIServiceEvent.ResetLayout]: true; [UIServiceEvent.ClearStorage]: true; [UIServiceEvent.InvalidateToken]: true; + [UIServiceEvent.OpenTask]: { taskId: string }; } diff --git a/apps/code/src/main/services/ui/service.ts b/apps/code/src/main/services/ui/service.ts index f991d4ea8..b43f915cd 100644 --- a/apps/code/src/main/services/ui/service.ts +++ b/apps/code/src/main/services/ui/service.ts @@ -33,4 +33,8 @@ export class UIService extends TypedEventEmitter { await this.authService.invalidateAccessTokenForTest(); this.emit(UIServiceEvent.InvalidateToken, true); } + + openTask(taskId: string): void { + this.emit(UIServiceEvent.OpenTask, { taskId }); + } } diff --git a/apps/code/src/main/trpc/router.ts b/apps/code/src/main/trpc/router.ts index 81fd00d4e..eda0355ef 100644 --- a/apps/code/src/main/trpc/router.ts +++ b/apps/code/src/main/trpc/router.ts @@ -28,6 +28,7 @@ import { oauthRouter } from "./routers/oauth"; import { osRouter } from "./routers/os"; import { processTrackingRouter } from "./routers/process-tracking"; import { provisioningRouter } from "./routers/provisioning"; +import { quickEntryRouter } from "./routers/quick-entry"; import { secureStoreRouter } from "./routers/secure-store"; import { shellRouter } from "./routers/shell"; import { skillsRouter } from "./routers/skills"; @@ -70,6 +71,7 @@ export const trpcRouter = router({ os: osRouter, processTracking: processTrackingRouter, provisioning: provisioningRouter, + quickEntry: quickEntryRouter, sleep: sleepRouter, suspension: suspensionRouter, secureStore: secureStoreRouter, diff --git a/apps/code/src/main/trpc/routers/quick-entry.ts b/apps/code/src/main/trpc/routers/quick-entry.ts new file mode 100644 index 000000000..29e77a672 --- /dev/null +++ b/apps/code/src/main/trpc/routers/quick-entry.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; +import { container } from "../../di/container"; +import { MAIN_TOKENS } from "../../di/tokens"; +import { + QuickEntryServiceEvent, + type QuickEntryServiceEvents, +} from "../../services/quick-entry/schemas"; +import type { QuickEntryService } from "../../services/quick-entry/service"; +import { publicProcedure, router } from "../trpc"; + +const getService = () => + container.get(MAIN_TOKENS.QuickEntryService); + +function subscribeToQuickEntryEvent( + event: K, +) { + return publicProcedure.subscription(async function* (opts) { + const service = getService(); + const iterable = service.toIterable(event, { signal: opts.signal }); + for await (const data of iterable) { + yield data; + } + }); +} + +export const quickEntryRouter = router({ + toggle: publicProcedure.mutation(() => { + getService().toggle(); + }), + + show: publicProcedure.mutation(() => { + getService().show(); + }), + + hide: publicProcedure.mutation(() => { + getService().hide(); + }), + + openTaskInMain: publicProcedure + .input(z.object({ taskId: z.string() })) + .mutation(({ input }) => { + getService().openTaskInMain(input.taskId); + }), + + getRecentRepos: publicProcedure + .input( + z.object({ limit: z.number().int().positive().optional() }).optional(), + ) + .query(({ input }) => { + return getService().getRecentRepos(input?.limit); + }), + + onFocusInput: subscribeToQuickEntryEvent(QuickEntryServiceEvent.FocusInput), + onHide: subscribeToQuickEntryEvent(QuickEntryServiceEvent.Hide), +}); diff --git a/apps/code/src/main/trpc/routers/ui.ts b/apps/code/src/main/trpc/routers/ui.ts index 45830580b..a8ef3b0b1 100644 --- a/apps/code/src/main/trpc/routers/ui.ts +++ b/apps/code/src/main/trpc/routers/ui.ts @@ -25,4 +25,5 @@ export const uiRouter = router({ onResetLayout: subscribeToUIEvent(UIServiceEvent.ResetLayout), onClearStorage: subscribeToUIEvent(UIServiceEvent.ClearStorage), onInvalidateToken: subscribeToUIEvent(UIServiceEvent.InvalidateToken), + onOpenTask: subscribeToUIEvent(UIServiceEvent.OpenTask), }); diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index d5796c939..b2662f0a1 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -18,6 +18,8 @@ import { isDevBuild } from "./utils/env"; import { logger, readChromiumLogTail } from "./utils/logger"; import { type WindowStateSchema, windowStateStore } from "./utils/store"; +type IPCHandlerHandle = ReturnType; + const log = logger.scope("window"); declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined; @@ -70,11 +72,20 @@ export function saveWindowState(window: BrowserWindow): void { } let mainWindow: BrowserWindow | null = null; +let ipcHandler: IPCHandlerHandle | null = null; export function getMainWindow(): BrowserWindow | null { return mainWindow; } +export function attachWindowToIPC(window: BrowserWindow): void { + if (!ipcHandler) { + log.warn("attachWindowToIPC called before IPC handler was created"); + return; + } + ipcHandler.attachWindow(window); +} + export function focusMainWindow(reason: string): void { if (mainWindow) { log.info("focusMainWindow called", { @@ -89,6 +100,148 @@ export function focusMainWindow(reason: string): void { } } +export function showAndFocusMainWindow(): void { + if (!mainWindow || mainWindow.isDestroyed()) return; + if (mainWindow.isMinimized()) mainWindow.restore(); + if (!mainWindow.isVisible()) mainWindow.show(); + mainWindow.moveTop(); + mainWindow.focus(); + app.focus({ steal: true }); +} + +// ===== Quick Entry window ===== + +const QUICK_ENTRY_WIDTH = 720; +const QUICK_ENTRY_HEIGHT = 132; +const QUICK_ENTRY_BOTTOM_MARGIN = 120; + +let quickEntryWindow: BrowserWindow | null = null; + +export interface QuickEntryWindowHandlers { + onBlur: () => void; +} + +export function createQuickEntryWindow( + handlers: QuickEntryWindowHandlers, +): void { + if (quickEntryWindow && !quickEntryWindow.isDestroyed()) return; + const isDev = isDevBuild(); + + const window = new BrowserWindow({ + width: QUICK_ENTRY_WIDTH, + height: QUICK_ENTRY_HEIGHT, + show: false, + frame: false, + transparent: true, + resizable: false, + movable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + skipTaskbar: true, + hasShadow: true, + roundedCorners: true, + alwaysOnTop: true, + backgroundColor: "#00000000", + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + preload: path.join(__dirname, "preload.js"), + partition: "persist:main", + additionalArguments: isDev + ? ["--posthog-code-dev", "--posthog-quick-entry"] + : ["--posthog-quick-entry"], + ...(isDev && { webSecurity: false }), + }, + }); + + window.setAlwaysOnTop(true, "floating"); + if (process.platform === "darwin") { + window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + } + + window.on("blur", () => { + if (!quickEntryWindow || quickEntryWindow.isDestroyed()) return; + handlers.onBlur(); + }); + + window.on("closed", () => { + if (quickEntryWindow === window) { + quickEntryWindow = null; + } + }); + + if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { + window.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}#quick-entry`); + } else { + window.loadFile( + path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`), + { hash: "quick-entry" }, + ); + } + + if (ipcHandler) { + ipcHandler.attachWindow(window); + } else { + log.warn("createQuickEntryWindow called before IPC handler exists"); + } + + quickEntryWindow = window; + log.info("Quick entry window created"); +} + +export function isQuickEntryWindowVisible(): boolean { + return ( + !!quickEntryWindow && + !quickEntryWindow.isDestroyed() && + quickEntryWindow.isVisible() + ); +} + +export function isQuickEntryWindowFocused(): boolean { + return ( + !!quickEntryWindow && + !quickEntryWindow.isDestroyed() && + quickEntryWindow.isFocused() + ); +} + +export function showQuickEntryWindow(): boolean { + const window = quickEntryWindow; + if (!window || window.isDestroyed()) { + log.warn("showQuickEntryWindow called before window exists"); + return false; + } + const cursor = screen.getCursorScreenPoint(); + const display = screen.getDisplayNearestPoint(cursor); + const { x: dx, y: dy, width: dw, height: dh } = display.workArea; + const x = Math.round(dx + (dw - QUICK_ENTRY_WIDTH) / 2); + const y = Math.round( + dy + dh - QUICK_ENTRY_HEIGHT - QUICK_ENTRY_BOTTOM_MARGIN, + ); + window.setPosition(x, y, false); + + window.show(); + window.focus(); + app.focus({ steal: true }); + return true; +} + +export function hideQuickEntryWindow(): void { + const window = quickEntryWindow; + if (!window || window.isDestroyed()) return; + if (!window.isVisible()) return; + window.hide(); +} + +export function destroyQuickEntryWindow(): void { + const window = quickEntryWindow; + quickEntryWindow = null; + if (window && !window.isDestroyed()) { + window.destroy(); + } +} + function setupExternalLinkHandlers(window: BrowserWindow): void { window.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); @@ -232,7 +385,7 @@ export function createWindow(): void { .get(MAIN_TOKENS.MainWindow) .setMainWindowGetter(() => mainWindow); - createIPCHandler({ + ipcHandler = createIPCHandler({ router: trpcRouter, windows: [mainWindow], }); diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index 8e8842eab..e6fed7cce 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -151,6 +151,19 @@ export function GlobalEventHandlers({ log.info("Main access token invalidated for testing"); }, []); + const handleOpenTask = useCallback( + (data?: unknown) => { + if (!data || typeof data !== "object") return; + const { taskId } = data as { taskId?: string }; + if (!taskId) return; + const task = taskById.get(taskId); + if (task) { + navigateToTask(task); + } + }, + [taskById, navigateToTask], + ); + const globalOptions = { enableOnFormTags: true, enableOnContentEditable: true, @@ -277,5 +290,11 @@ export function GlobalEventHandlers({ }), ); + useSubscription( + trpcReact.ui.onOpenTask.subscriptionOptions(undefined, { + onData: handleOpenTask, + }), + ); + return null; } diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryRoot.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryRoot.tsx new file mode 100644 index 000000000..d7b4897b7 --- /dev/null +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryRoot.tsx @@ -0,0 +1,22 @@ +import { ErrorBoundary } from "@components/ErrorBoundary"; +import { useThemeStore } from "@renderer/stores/themeStore"; +import { useEffect } from "react"; +import { QuickEntryView } from "./QuickEntryView"; + +export function QuickEntryRoot() { + const isDarkMode = useThemeStore((state) => state.isDarkMode); + + useEffect(() => { + document.documentElement.classList.toggle("dark", isDarkMode); + document.documentElement.style.backgroundColor = "transparent"; + document.body.style.backgroundColor = "transparent"; + }, [isDarkMode]); + + return ( + +
+ +
+
+ ); +} diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx new file mode 100644 index 000000000..a3657d49a --- /dev/null +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx @@ -0,0 +1,251 @@ +import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { PromptInput } from "@features/message-editor/components/PromptInput"; +import type { EditorHandle } from "@features/message-editor/types"; +import { contentToXml } from "@features/message-editor/utils/content"; +import { useSettingsStore } from "@features/settings/stores/settingsStore"; +import { + CaretDown, + Folder as FolderIcon, + Lightning, +} from "@phosphor-icons/react"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + MenuLabel, +} from "@posthog/quill"; +import { Flex, Text } from "@radix-ui/themes"; +import { get } from "@renderer/di/container"; +import { RENDERER_TOKENS } from "@renderer/di/tokens"; +import type { TaskService } from "@renderer/features/task-detail/service/service"; +import { trpcClient, useTRPC } from "@renderer/trpc/client"; +import { useQuery } from "@tanstack/react-query"; +import { useSubscription } from "@trpc/tanstack-react-query"; +import { logger } from "@utils/logger"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +const log = logger.scope("quick-entry-view"); + +function hideWindow(): void { + trpcClient.quickEntry.hide.mutate().catch((err) => { + log.warn("Failed to hide quick entry window", { err }); + }); +} + +export function QuickEntryView() { + const trpcReact = useTRPC(); + const editorRef = useRef(null); + const [selectedRepoPath, setSelectedRepoPath] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + const isAuthenticated = useAuthStateValue( + (state) => state.status === "authenticated", + ); + + const lastUsedWorkspaceMode = useSettingsStore( + (s) => s.lastUsedWorkspaceMode, + ); + const lastUsedAdapter = useSettingsStore((s) => s.lastUsedAdapter); + const lastUsedModel = useSettingsStore((s) => s.lastUsedModel); + const lastUsedReasoningEffort = useSettingsStore( + (s) => s.lastUsedReasoningEffort, + ); + + const { data: recentRepos = [] } = useQuery({ + ...trpcReact.quickEntry.getRecentRepos.queryOptions({ limit: 8 }), + }); + + useEffect(() => { + if (selectedRepoPath) return; + const first = recentRepos[0]; + if (first) setSelectedRepoPath(first.path); + }, [recentRepos, selectedRepoPath]); + + const selectedRepo = + recentRepos.find((r) => r.path === selectedRepoPath) ?? null; + + useSubscription( + trpcReact.quickEntry.onFocusInput.subscriptionOptions(undefined, { + onData: () => { + editorRef.current?.focus(); + }, + }), + ); + + useHotkeys( + "escape", + () => { + hideWindow(); + }, + { + enableOnFormTags: true, + enableOnContentEditable: true, + }, + ); + + const handleSubmit = useCallback(async () => { + const editor = editorRef.current; + if (!editor || isSubmitting) return; + + const content = editor.getContent(); + const xml = contentToXml(content).trim(); + if (!xml) return; + + if (!selectedRepoPath) { + setError("Pick a folder first"); + return; + } + if (!isAuthenticated) { + setError("Sign in to PostHog Code first"); + return; + } + + setError(null); + setIsSubmitting(true); + + try { + const workspaceMode = + lastUsedWorkspaceMode === "cloud" ? "local" : lastUsedWorkspaceMode; + const taskService = get(RENDERER_TOKENS.TaskService); + const result = await taskService.createTask({ + content: xml, + repoPath: selectedRepoPath, + workspaceMode, + adapter: lastUsedAdapter, + model: lastUsedModel ?? undefined, + reasoningLevel: lastUsedReasoningEffort ?? undefined, + }); + + if (!result.success) { + setError(result.error ?? "Failed to create task"); + log.error("Quick entry task creation failed", { + failedStep: result.failedStep, + error: result.error, + }); + return; + } + + const taskId = result.data.task.id; + editor.clear(); + await trpcClient.quickEntry.openTaskInMain.mutate({ taskId }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + log.error("Quick entry submit threw", { err }); + } finally { + setIsSubmitting(false); + } + }, [ + isSubmitting, + selectedRepoPath, + isAuthenticated, + lastUsedWorkspaceMode, + lastUsedAdapter, + lastUsedModel, + lastUsedReasoningEffort, + ]); + + const repoDisplay = selectedRepo + ? selectedRepo.name + : recentRepos.length === 0 + ? "No folders yet" + : "Pick a folder"; + + if (!isAuthenticated) { + return ( +
+
+ + + Sign in to PostHog Code to use quick entry. + + +
+
+ ); + } + + return ( +
+
+ + +
+ { + void handleSubmit(); + }} + onSubmitClick={() => { + void handleSubmit(); + }} + isLoading={isSubmitting} + submitDisabledExternal={!selectedRepoPath || isSubmitting} + submitTooltipOverride={ + !selectedRepoPath ? "Pick a folder first" : "Send" + } + editorHeight="default" + /> +
+ + + + + + Recent folders + {recentRepos.map((repo) => ( + setSelectedRepoPath(repo.path)} + > + + + {repo.name} + + + {repo.path} + + + + ))} + + +
+ {error && {error}} +
+
+ ); +} diff --git a/apps/code/src/renderer/main.tsx b/apps/code/src/renderer/main.tsx index c3a7fce92..7443ee699 100644 --- a/apps/code/src/renderer/main.tsx +++ b/apps/code/src/renderer/main.tsx @@ -4,30 +4,35 @@ import "@stores/rendererWindowFocusStore"; import { Providers } from "@components/Providers"; import { preloadHighlighter } from "@pierre/diffs"; import App from "@renderer/App"; +import { QuickEntryRoot } from "@renderer/features/quick-entry/QuickEntryRoot"; import React from "react"; import ReactDOM from "react-dom/client"; import "./styles/globals.css"; -void preloadHighlighter({ - themes: ["github-dark", "github-light"], - langs: [ - "typescript", - "tsx", - "javascript", - "jsx", - "json", - "css", - "html", - "markdown", - "python", - "ruby", - "go", - "rust", - "shell", - "yaml", - "sql", - ], -}); +const isQuickEntry = window.location.hash === "#quick-entry"; + +if (!isQuickEntry) { + void preloadHighlighter({ + themes: ["github-dark", "github-light"], + langs: [ + "typescript", + "tsx", + "javascript", + "jsx", + "json", + "css", + "html", + "markdown", + "python", + "ruby", + "go", + "rust", + "shell", + "yaml", + "sql", + ], + }); +} // HACK(@posthog/hedgehog-mode): The package bundles react-dom 18 code that // accesses React 18 internals at module scope. React 19 moved these to @@ -55,17 +60,17 @@ void preloadHighlighter({ } } -document.title = import.meta.env.DEV - ? "PostHog Code (Development)" - : "PostHog Code"; +document.title = isQuickEntry + ? "PostHog Code Quick Entry" + : import.meta.env.DEV + ? "PostHog Code (Development)" + : "PostHog Code"; const rootElement = document.getElementById("root"); if (!rootElement) throw new Error("Root element not found"); ReactDOM.createRoot(rootElement).render( - - - + {isQuickEntry ? : } , ); From b3e2fe4b52799295e7bf43427513c97d7974b542 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 22 May 2026 16:15:44 -0300 Subject: [PATCH 2/8] refactor(code): match quick entry widget to actual TaskInput layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape the quick-entry view to mirror the real TaskInput: - Drop the orange Lightning icon and inline pill design. - Header row with FolderPicker inside ButtonGroup (same as TaskInput). - Full PromptInput below with the standard toolbar: UnifiedModelSelector, ReasoningLevelSelector, mode selector (via usePreviewConfig), AttachmentMenu, history-aware hints. - Placeholder matches the real one: "What do you want to ship? @ to add files, / for skills, ↑↓ for history". - Loads skills into useDraftStore so `/` for skills works. - Bump window to 680×260 to fit the taller layout. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- apps/code/src/main/window.ts | 4 +- .../features/quick-entry/QuickEntryView.tsx | 330 +++++++++++------- 2 files changed, 205 insertions(+), 129 deletions(-) diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index b2662f0a1..c7a851fab 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -111,8 +111,8 @@ export function showAndFocusMainWindow(): void { // ===== Quick Entry window ===== -const QUICK_ENTRY_WIDTH = 720; -const QUICK_ENTRY_HEIGHT = 132; +const QUICK_ENTRY_WIDTH = 680; +const QUICK_ENTRY_HEIGHT = 260; const QUICK_ENTRY_BOTTOM_MARGIN = 120; let quickEntryWindow: BrowserWindow | null = null; diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx index a3657d49a..c0233df11 100644 --- a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx @@ -1,33 +1,28 @@ import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { FolderPicker } from "@features/folder-picker/components/FolderPicker"; import { PromptInput } from "@features/message-editor/components/PromptInput"; +import { useDraftStore } from "@features/message-editor/stores/draftStore"; +import { useTaskInputHistoryStore } from "@features/message-editor/stores/taskInputHistoryStore"; import type { EditorHandle } from "@features/message-editor/types"; import { contentToXml } from "@features/message-editor/utils/content"; +import { ReasoningLevelSelector } from "@features/sessions/components/ReasoningLevelSelector"; +import { UnifiedModelSelector } from "@features/sessions/components/UnifiedModelSelector"; +import { getCurrentModeFromConfigOptions } from "@features/sessions/stores/sessionStore"; import { useSettingsStore } from "@features/settings/stores/settingsStore"; -import { - CaretDown, - Folder as FolderIcon, - Lightning, -} from "@phosphor-icons/react"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - MenuLabel, -} from "@posthog/quill"; +import { ButtonGroup } from "@posthog/quill"; import { Flex, Text } from "@radix-ui/themes"; import { get } from "@renderer/di/container"; import { RENDERER_TOKENS } from "@renderer/di/tokens"; +import { usePreviewConfig } from "@renderer/features/task-detail/hooks/usePreviewConfig"; import type { TaskService } from "@renderer/features/task-detail/service/service"; import { trpcClient, useTRPC } from "@renderer/trpc/client"; -import { useQuery } from "@tanstack/react-query"; import { useSubscription } from "@trpc/tanstack-react-query"; import { logger } from "@utils/logger"; import { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; const log = logger.scope("quick-entry-view"); +const SESSION_ID = "quick-entry"; function hideWindow(): void { trpcClient.quickEntry.hide.mutate().catch((err) => { @@ -38,35 +33,70 @@ function hideWindow(): void { export function QuickEntryView() { const trpcReact = useTRPC(); const editorRef = useRef(null); - const [selectedRepoPath, setSelectedRepoPath] = useState(null); + const [selectedDirectory, setSelectedDirectory] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); + const [editorIsEmpty, setEditorIsEmpty] = useState(true); const isAuthenticated = useAuthStateValue( (state) => state.status === "authenticated", ); - const lastUsedWorkspaceMode = useSettingsStore( - (s) => s.lastUsedWorkspaceMode, - ); - const lastUsedAdapter = useSettingsStore((s) => s.lastUsedAdapter); - const lastUsedModel = useSettingsStore((s) => s.lastUsedModel); - const lastUsedReasoningEffort = useSettingsStore( - (s) => s.lastUsedReasoningEffort, - ); + const { + lastUsedAdapter, + setLastUsedAdapter, + lastUsedWorkspaceMode, + defaultInitialTaskMode, + lastUsedInitialTaskMode, + setLastUsedReasoningEffort, + } = useSettingsStore(); - const { data: recentRepos = [] } = useQuery({ - ...trpcReact.quickEntry.getRecentRepos.queryOptions({ limit: 8 }), - }); + const adapter = lastUsedAdapter ?? "claude"; + + const { + modeOption, + modelOption, + thoughtOption, + isLoading: isPreviewLoading, + setConfigOption, + } = usePreviewConfig(adapter); + // Seed default folder once from the most-recently-accessed repository. useEffect(() => { - if (selectedRepoPath) return; - const first = recentRepos[0]; - if (first) setSelectedRepoPath(first.path); - }, [recentRepos, selectedRepoPath]); + if (selectedDirectory) return; + let cancelled = false; + trpcClient.folders.getMostRecentlyAccessedRepository + .query() + .then((repo) => { + if (cancelled || !repo) return; + setSelectedDirectory(repo.path); + }) + .catch(() => { + // ignore — user can still pick manually + }); + return () => { + cancelled = true; + }; + }, [selectedDirectory]); - const selectedRepo = - recentRepos.find((r) => r.path === selectedRepoPath) ?? null; + // Populate command list for @ file mentions + / skills. + useEffect(() => { + let cancelled = false; + trpcClient.skills.list.query().then((skills) => { + if (cancelled) return; + useDraftStore.getState().actions.setCommands( + SESSION_ID, + skills.map((s) => ({ + name: s.name, + description: s.description, + })), + ); + }); + return () => { + cancelled = true; + useDraftStore.getState().actions.clearCommands(SESSION_ID); + }; + }, []); useSubscription( trpcReact.quickEntry.onFocusInput.subscriptionOptions(undefined, { @@ -87,15 +117,50 @@ export function QuickEntryView() { }, ); + const hasHistory = useTaskInputHistoryStore((s) => s.entries.length > 0); + const hints = [ + "@ to add files", + "/ for skills", + hasHistory ? "↑↓ for history" : "", + ] + .filter(Boolean) + .join(", "); + + const handleModeChange = useCallback( + (value: string) => { + if (modeOption) setConfigOption(modeOption.id, value); + }, + [modeOption, setConfigOption], + ); + + const handleModelChange = useCallback( + (value: string) => { + if (modelOption) setConfigOption(modelOption.id, value); + }, + [modelOption, setConfigOption], + ); + + const handleThoughtChange = useCallback( + (value: string) => { + if (thoughtOption) { + setConfigOption(thoughtOption.id, value); + setLastUsedReasoningEffort(value); + } + }, + [thoughtOption, setConfigOption, setLastUsedReasoningEffort], + ); + + const canSubmit = + !!editorRef.current && + !!selectedDirectory && + !editorIsEmpty && + !isSubmitting; + const handleSubmit = useCallback(async () => { const editor = editorRef.current; if (!editor || isSubmitting) return; - const content = editor.getContent(); - const xml = contentToXml(content).trim(); - if (!xml) return; - - if (!selectedRepoPath) { + if (!selectedDirectory) { setError("Pick a folder first"); return; } @@ -104,20 +169,46 @@ export function QuickEntryView() { return; } + const content = editor.getContent(); + const xml = contentToXml(content).trim(); + if (!xml) return; + + const plainText = editor.getText()?.trim(); + if (plainText) { + useTaskInputHistoryStore.getState().addPrompt(plainText); + } + setError(null); setIsSubmitting(true); try { const workspaceMode = lastUsedWorkspaceMode === "cloud" ? "local" : lastUsedWorkspaceMode; + const currentModel = + modelOption?.type === "select" ? modelOption.currentValue : undefined; + const currentReasoningLevel = + thoughtOption?.type === "select" + ? thoughtOption.currentValue + : undefined; + const adapterDefault = adapter === "codex" ? "auto" : "plan"; + const modeFallback = + defaultInitialTaskMode === "last_used" + ? (lastUsedInitialTaskMode ?? adapterDefault) + : adapterDefault; + const currentExecutionMode = + getCurrentModeFromConfigOptions( + modeOption ? [modeOption] : undefined, + ) ?? modeFallback; + const taskService = get(RENDERER_TOKENS.TaskService); const result = await taskService.createTask({ content: xml, - repoPath: selectedRepoPath, + repoPath: selectedDirectory, workspaceMode, - adapter: lastUsedAdapter, - model: lastUsedModel ?? undefined, - reasoningLevel: lastUsedReasoningEffort ?? undefined, + adapter, + model: currentModel, + reasoningLevel: currentReasoningLevel, + executionMode: currentExecutionMode, }); if (!result.success) { @@ -141,110 +232,95 @@ export function QuickEntryView() { } }, [ isSubmitting, - selectedRepoPath, + selectedDirectory, isAuthenticated, + adapter, lastUsedWorkspaceMode, - lastUsedAdapter, - lastUsedModel, - lastUsedReasoningEffort, + modelOption, + thoughtOption, + modeOption, + defaultInitialTaskMode, + lastUsedInitialTaskMode, ]); - const repoDisplay = selectedRepo - ? selectedRepo.name - : recentRepos.length === 0 - ? "No folders yet" - : "Pick a folder"; + const getPromptHistory = useCallback( + () => useTaskInputHistoryStore.getState().entries.map((e) => e.text), + [], + ); if (!isAuthenticated) { return ( -
-
- +
+
Sign in to PostHog Code to use quick entry. -
); } return ( -
-
- - -
- { - void handleSubmit(); - }} - onSubmitClick={() => { - void handleSubmit(); - }} - isLoading={isSubmitting} - submitDisabledExternal={!selectedRepoPath || isSubmitting} - submitTooltipOverride={ - !selectedRepoPath ? "Pick a folder first" : "Send" - } - editorHeight="default" +
+
+ + + -
- - - - - - Recent folders - {recentRepos.map((repo) => ( - setSelectedRepoPath(repo.path)} - > - - - {repo.name} - - - {repo.path} - - - - ))} - - + + + + + + } + reasoningSelector={ + !isPreviewLoading && ( + + ) + } + getPromptHistory={getPromptHistory} + onEmptyChange={setEditorIsEmpty} + onSubmitClick={() => { + void handleSubmit(); + }} + onSubmit={() => { + if (canSubmit) void handleSubmit(); + }} + /> - {error && {error}} + + {error && {error}}
); From 4be020fd83aab4109fcae187c6828c1b57a3868a Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 22 May 2026 16:26:07 -0300 Subject: [PATCH 3/8] style(code): widen quick entry and drop outer card Remove the rounded/bordered wrapper around the quick entry contents so only the PromptInput's own card surface shows. Widen the BrowserWindow from 680 to 960 to fit the actual TaskInput-style layout. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- apps/code/src/main/window.ts | 2 +- .../renderer/features/quick-entry/QuickEntryView.tsx | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index c7a851fab..199b3b0ce 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -111,7 +111,7 @@ export function showAndFocusMainWindow(): void { // ===== Quick Entry window ===== -const QUICK_ENTRY_WIDTH = 680; +const QUICK_ENTRY_WIDTH = 960; const QUICK_ENTRY_HEIGHT = 260; const QUICK_ENTRY_BOTTOM_MARGIN = 120; diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx index c0233df11..aa056fdf6 100644 --- a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx @@ -251,18 +251,16 @@ export function QuickEntryView() { if (!isAuthenticated) { return (
-
- - Sign in to PostHog Code to use quick entry. - -
+ + Sign in to PostHog Code to use quick entry. +
); } return (
-
+
Date: Fri, 22 May 2026 16:34:26 -0300 Subject: [PATCH 4/8] fix(code): quick entry branch picker, task visibility, clear-on-hide - Add BranchSelector next to FolderPicker, wired via useGitQueries. Cloud workspace mode is coerced to worktree from quick entry (no cloud repo picker here), so the selected branch is honored on submit. - Fix the main window not seeing the new task after submit: when the onOpenTask event fires with a taskId that isn't in the main window's React Query cache yet, store it as pendingOpenTaskId, invalidate the tasks query, and navigate when the task lands in taskById. - Clear the editor and error message when the quick entry window hides, so reopening starts fresh. - Shrink the window from 260 to 200 tall and switch the outer wrapper to justify-center so the content hugs the top instead of floating mid-window. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- apps/code/src/main/window.ts | 2 +- .../components/GlobalEventHandlers.tsx | 24 +++++++-- .../features/quick-entry/QuickEntryView.tsx | 50 +++++++++++++++++-- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 199b3b0ce..da8471213 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -112,7 +112,7 @@ export function showAndFocusMainWindow(): void { // ===== Quick Entry window ===== const QUICK_ENTRY_WIDTH = 960; -const QUICK_ENTRY_HEIGHT = 260; +const QUICK_ENTRY_HEIGHT = 200; const QUICK_ENTRY_BOTTOM_MARGIN = 120; let quickEntryWindow: BrowserWindow | null = null; diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index e6fed7cce..15b035b95 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -6,7 +6,7 @@ import { useSettingsDialogStore } from "@features/settings/stores/settingsDialog import { useSidebarData } from "@features/sidebar/hooks/useSidebarData"; import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder"; import { useSidebarStore } from "@features/sidebar/stores/sidebarStore"; -import { useTasks } from "@features/tasks/hooks/useTasks"; +import { useCreateTask, useTasks } from "@features/tasks/hooks/useTasks"; import { useFocusWorkspace } from "@features/workspace/hooks/useFocusWorkspace"; import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts"; @@ -17,7 +17,7 @@ import { useNavigationStore } from "@stores/navigationStore"; import { useSubscription } from "@trpc/tanstack-react-query"; import { clearApplicationStorage } from "@utils/clearStorage"; import { logger } from "@utils/logger"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; interface GlobalEventHandlersProps { @@ -61,6 +61,10 @@ export function GlobalEventHandlers({ const isWorktreeTask = currentWorkspace?.mode === "worktree"; const { data: allTasks = [] } = useTasks(); + const { invalidateTasks } = useCreateTask(); + const [pendingOpenTaskId, setPendingOpenTaskId] = useState( + null, + ); const sidebarData = useSidebarData({ activeView: view }); const visualTaskOrder = useVisualTaskOrder(sidebarData); @@ -159,11 +163,25 @@ export function GlobalEventHandlers({ const task = taskById.get(taskId); if (task) { navigateToTask(task); + } else { + // Task was created in another window (e.g. quick-entry). Refetch the + // task list and navigate once it lands in the cache. + setPendingOpenTaskId(taskId); + invalidateTasks(); } }, - [taskById, navigateToTask], + [taskById, navigateToTask, invalidateTasks], ); + useEffect(() => { + if (!pendingOpenTaskId) return; + const task = taskById.get(pendingOpenTaskId); + if (task) { + navigateToTask(task); + setPendingOpenTaskId(null); + } + }, [pendingOpenTaskId, taskById, navigateToTask]); + const globalOptions = { enableOnFormTags: true, enableOnContentEditable: true, diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx index aa056fdf6..a9aacd9d5 100644 --- a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx @@ -1,5 +1,7 @@ import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { FolderPicker } from "@features/folder-picker/components/FolderPicker"; +import { BranchSelector } from "@features/git-interaction/components/BranchSelector"; +import { useGitQueries } from "@features/git-interaction/hooks/useGitQueries"; import { PromptInput } from "@features/message-editor/components/PromptInput"; import { useDraftStore } from "@features/message-editor/stores/draftStore"; import { useTaskInputHistoryStore } from "@features/message-editor/stores/taskInputHistoryStore"; @@ -34,10 +36,14 @@ export function QuickEntryView() { const trpcReact = useTRPC(); const editorRef = useRef(null); const [selectedDirectory, setSelectedDirectory] = useState(""); + const [selectedBranch, setSelectedBranch] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [editorIsEmpty, setEditorIsEmpty] = useState(true); + const { currentBranch, branchLoading, defaultBranch, busyState } = + useGitQueries(selectedDirectory); + const isAuthenticated = useAuthStateValue( (state) => state.status === "authenticated", ); @@ -52,6 +58,13 @@ export function QuickEntryView() { } = useSettingsStore(); const adapter = lastUsedAdapter ?? "claude"; + // Cloud isn't supported from quick entry (no cloud repo picker here). + // Default to worktree so branch selection is meaningful; otherwise use + // the user's preferred local mode. + const effectiveWorkspaceMode: "worktree" | "local" = + lastUsedWorkspaceMode === "cloud" + ? "worktree" + : (lastUsedWorkspaceMode as "worktree" | "local"); const { modeOption, @@ -106,6 +119,21 @@ export function QuickEntryView() { }), ); + useSubscription( + trpcReact.quickEntry.onHide.subscriptionOptions(undefined, { + onData: () => { + editorRef.current?.clear(); + setError(null); + }, + }), + ); + + // Reset branch selection when the repo changes. + // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally only reset when repo changes + useEffect(() => { + setSelectedBranch(null); + }, [selectedDirectory]); + useHotkeys( "escape", () => { @@ -182,8 +210,9 @@ export function QuickEntryView() { setIsSubmitting(true); try { - const workspaceMode = - lastUsedWorkspaceMode === "cloud" ? "local" : lastUsedWorkspaceMode; + const workspaceMode = effectiveWorkspaceMode; + const branchForTaskCreation = + workspaceMode === "worktree" ? selectedBranch : null; const currentModel = modelOption?.type === "select" ? modelOption.currentValue : undefined; const currentReasoningLevel = @@ -205,6 +234,7 @@ export function QuickEntryView() { content: xml, repoPath: selectedDirectory, workspaceMode, + branch: branchForTaskCreation, adapter, model: currentModel, reasoningLevel: currentReasoningLevel, @@ -233,9 +263,10 @@ export function QuickEntryView() { }, [ isSubmitting, selectedDirectory, + selectedBranch, isAuthenticated, adapter, - lastUsedWorkspaceMode, + effectiveWorkspaceMode, modelOption, thoughtOption, modeOption, @@ -259,7 +290,7 @@ export function QuickEntryView() { } return ( -
+
@@ -268,6 +299,17 @@ export function QuickEntryView() { onChange={setSelectedDirectory} placeholder="Select repository..." /> + From 43eb7e12fdd596220dd79151d379a2a9742fab69 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 22 May 2026 16:40:26 -0300 Subject: [PATCH 5/8] fix(code): invalidate workspace/folders caches + tighten quick entry size - When onOpenTask fires in the main window, also invalidate workspace.getAll and folders.getFolders so the task-detail view sees the new workspace/folder created in the quick-entry window. Without this the task opened to the "Select a repository folder" empty state. - Shrink the quick entry window from 200 to 170 tall and switch the outer wrapper to items-start so the inner card hugs the top of the window instead of stretching, removing the empty space below. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- apps/code/src/main/window.ts | 2 +- .../renderer/components/GlobalEventHandlers.tsx | 15 ++++++++++++--- .../features/quick-entry/QuickEntryView.tsx | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index da8471213..55e618871 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -112,7 +112,7 @@ export function showAndFocusMainWindow(): void { // ===== Quick Entry window ===== const QUICK_ENTRY_WIDTH = 960; -const QUICK_ENTRY_HEIGHT = 200; +const QUICK_ENTRY_HEIGHT = 170; const QUICK_ENTRY_BOTTOM_MARGIN = 120; let quickEntryWindow: BrowserWindow | null = null; diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index 15b035b95..e7227bd9d 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -14,6 +14,7 @@ import { useTRPC } from "@renderer/trpc"; import type { Task } from "@shared/types"; import { useCommandMenuStore } from "@stores/commandMenuStore"; import { useNavigationStore } from "@stores/navigationStore"; +import { useQueryClient } from "@tanstack/react-query"; import { useSubscription } from "@trpc/tanstack-react-query"; import { clearApplicationStorage } from "@utils/clearStorage"; import { logger } from "@utils/logger"; @@ -62,6 +63,7 @@ export function GlobalEventHandlers({ const { data: allTasks = [] } = useTasks(); const { invalidateTasks } = useCreateTask(); + const queryClient = useQueryClient(); const [pendingOpenTaskId, setPendingOpenTaskId] = useState( null, ); @@ -160,17 +162,24 @@ export function GlobalEventHandlers({ if (!data || typeof data !== "object") return; const { taskId } = data as { taskId?: string }; if (!taskId) return; + // The task may have been created in another window (e.g. quick-entry), + // so the main window's caches for workspaces and folders can be stale + // and miss the new workspace/folder. Invalidate before navigating. + void queryClient.invalidateQueries( + trpcReact.workspace.getAll.pathFilter(), + ); + void queryClient.invalidateQueries( + trpcReact.folders.getFolders.pathFilter(), + ); const task = taskById.get(taskId); if (task) { navigateToTask(task); } else { - // Task was created in another window (e.g. quick-entry). Refetch the - // task list and navigate once it lands in the cache. setPendingOpenTaskId(taskId); invalidateTasks(); } }, - [taskById, navigateToTask, invalidateTasks], + [taskById, navigateToTask, invalidateTasks, queryClient, trpcReact], ); useEffect(() => { diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx index a9aacd9d5..3457fd20b 100644 --- a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx @@ -290,7 +290,7 @@ export function QuickEntryView() { } return ( -
+
From 2dfe170c07131a0d9d40b14102a8fbeae56b3824 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 22 May 2026 16:58:12 -0300 Subject: [PATCH 6/8] fix(code): run quick-entry task creation in the main window Previously the QE renderer ran the full TaskCreationSaga, which writes to renderer-local Zustand stores for session, draft, and folder caches. The main window then navigated to the new task with no local session record, so useSessionConnection bailed out (it doesn't auto-start sessions for brand-new tasks) and the task detail rendered with stale/empty state. New flow: - QE collects the form params (prompt XML, repo, branch, adapter, model, reasoning, executionMode) and calls a new trpc.quickEntry.requestCreateTask mutation. - QuickEntryService hides the QE window, focuses the main window, and emits a CreateTaskRequested event with the params. - The main window subscribes via trpc.quickEntry.onCreateTaskRequested and runs TaskService.createTask in its own renderer, with the onTaskReady callback navigating to the new task. All renderer-local state (session manager, folder cache, sidebar, navigation) is set up in the right window. Removed the now-unused UIService.OpenTask event, UIService.openTask(), uiRouter.onOpenTask, QuickEntryService.openTaskInMain() and the quickEntry.openTaskInMain mutation. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- .../src/main/services/quick-entry/schemas.ts | 13 +++ .../src/main/services/quick-entry/service.ts | 10 +- apps/code/src/main/services/ui/schemas.ts | 2 - apps/code/src/main/services/ui/service.ts | 4 - .../code/src/main/trpc/routers/quick-entry.ts | 20 +++- apps/code/src/main/trpc/routers/ui.ts | 1 - .../components/GlobalEventHandlers.tsx | 97 +++++++++++-------- .../features/quick-entry/QuickEntryView.tsx | 25 +---- 8 files changed, 95 insertions(+), 77 deletions(-) diff --git a/apps/code/src/main/services/quick-entry/schemas.ts b/apps/code/src/main/services/quick-entry/schemas.ts index 3dd40579c..3e7c871ef 100644 --- a/apps/code/src/main/services/quick-entry/schemas.ts +++ b/apps/code/src/main/services/quick-entry/schemas.ts @@ -1,11 +1,24 @@ export const QuickEntryServiceEvent = { FocusInput: "focus-input", Hide: "hide", + CreateTaskRequested: "create-task-requested", } as const; +export interface CreateTaskRequest { + content: string; + repoPath: string; + workspaceMode: "local" | "worktree"; + branch: string | null; + adapter: "claude" | "codex"; + model: string | null; + reasoningLevel: string | null; + executionMode: string | null; +} + export interface QuickEntryServiceEvents { [QuickEntryServiceEvent.FocusInput]: true; [QuickEntryServiceEvent.Hide]: true; + [QuickEntryServiceEvent.CreateTaskRequested]: CreateTaskRequest; } export interface RecentRepoEntry { diff --git a/apps/code/src/main/services/quick-entry/service.ts b/apps/code/src/main/services/quick-entry/service.ts index 38097c181..769705375 100644 --- a/apps/code/src/main/services/quick-entry/service.ts +++ b/apps/code/src/main/services/quick-entry/service.ts @@ -12,8 +12,8 @@ import { showQuickEntryWindow, } from "../../window"; import type { FoldersService } from "../folders/service"; -import type { UIService } from "../ui/service"; import { + type CreateTaskRequest, QuickEntryServiceEvent, type QuickEntryServiceEvents, type RecentRepoEntry, @@ -30,8 +30,6 @@ export class QuickEntryService extends TypedEventEmitter { diff --git a/apps/code/src/main/services/ui/schemas.ts b/apps/code/src/main/services/ui/schemas.ts index d882e1505..ce77eecb9 100644 --- a/apps/code/src/main/services/ui/schemas.ts +++ b/apps/code/src/main/services/ui/schemas.ts @@ -5,7 +5,6 @@ export const UIServiceEvent = { ResetLayout: "reset-layout", ClearStorage: "clear-storage", InvalidateToken: "invalidate-token", - OpenTask: "open-task", } as const; // UI events are simple signals - payload is just a marker that the event fired @@ -15,5 +14,4 @@ export interface UIServiceEvents { [UIServiceEvent.ResetLayout]: true; [UIServiceEvent.ClearStorage]: true; [UIServiceEvent.InvalidateToken]: true; - [UIServiceEvent.OpenTask]: { taskId: string }; } diff --git a/apps/code/src/main/services/ui/service.ts b/apps/code/src/main/services/ui/service.ts index b43f915cd..f991d4ea8 100644 --- a/apps/code/src/main/services/ui/service.ts +++ b/apps/code/src/main/services/ui/service.ts @@ -33,8 +33,4 @@ export class UIService extends TypedEventEmitter { await this.authService.invalidateAccessTokenForTest(); this.emit(UIServiceEvent.InvalidateToken, true); } - - openTask(taskId: string): void { - this.emit(UIServiceEvent.OpenTask, { taskId }); - } } diff --git a/apps/code/src/main/trpc/routers/quick-entry.ts b/apps/code/src/main/trpc/routers/quick-entry.ts index 29e77a672..d1e77a39e 100644 --- a/apps/code/src/main/trpc/routers/quick-entry.ts +++ b/apps/code/src/main/trpc/routers/quick-entry.ts @@ -23,6 +23,17 @@ function subscribeToQuickEntryEvent( }); } +const createTaskRequestInput = z.object({ + content: z.string(), + repoPath: z.string(), + workspaceMode: z.enum(["local", "worktree"]), + branch: z.string().nullable(), + adapter: z.enum(["claude", "codex"]), + model: z.string().nullable(), + reasoningLevel: z.string().nullable(), + executionMode: z.string().nullable(), +}); + export const quickEntryRouter = router({ toggle: publicProcedure.mutation(() => { getService().toggle(); @@ -36,10 +47,10 @@ export const quickEntryRouter = router({ getService().hide(); }), - openTaskInMain: publicProcedure - .input(z.object({ taskId: z.string() })) + requestCreateTask: publicProcedure + .input(createTaskRequestInput) .mutation(({ input }) => { - getService().openTaskInMain(input.taskId); + getService().requestCreateTask(input); }), getRecentRepos: publicProcedure @@ -52,4 +63,7 @@ export const quickEntryRouter = router({ onFocusInput: subscribeToQuickEntryEvent(QuickEntryServiceEvent.FocusInput), onHide: subscribeToQuickEntryEvent(QuickEntryServiceEvent.Hide), + onCreateTaskRequested: subscribeToQuickEntryEvent( + QuickEntryServiceEvent.CreateTaskRequested, + ), }); diff --git a/apps/code/src/main/trpc/routers/ui.ts b/apps/code/src/main/trpc/routers/ui.ts index a8ef3b0b1..45830580b 100644 --- a/apps/code/src/main/trpc/routers/ui.ts +++ b/apps/code/src/main/trpc/routers/ui.ts @@ -25,5 +25,4 @@ export const uiRouter = router({ onResetLayout: subscribeToUIEvent(UIServiceEvent.ResetLayout), onClearStorage: subscribeToUIEvent(UIServiceEvent.ClearStorage), onInvalidateToken: subscribeToUIEvent(UIServiceEvent.InvalidateToken), - onOpenTask: subscribeToUIEvent(UIServiceEvent.OpenTask), }); diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index e7227bd9d..2b85c0300 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -6,19 +6,22 @@ import { useSettingsDialogStore } from "@features/settings/stores/settingsDialog import { useSidebarData } from "@features/sidebar/hooks/useSidebarData"; import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder"; import { useSidebarStore } from "@features/sidebar/stores/sidebarStore"; -import { useCreateTask, useTasks } from "@features/tasks/hooks/useTasks"; +import { useTasks } from "@features/tasks/hooks/useTasks"; import { useFocusWorkspace } from "@features/workspace/hooks/useFocusWorkspace"; import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts"; +import { get } from "@renderer/di/container"; +import { RENDERER_TOKENS } from "@renderer/di/tokens"; +import type { TaskService } from "@renderer/features/task-detail/service/service"; import { useTRPC } from "@renderer/trpc"; -import type { Task } from "@shared/types"; +import type { ExecutionMode, Task } from "@shared/types"; import { useCommandMenuStore } from "@stores/commandMenuStore"; import { useNavigationStore } from "@stores/navigationStore"; -import { useQueryClient } from "@tanstack/react-query"; import { useSubscription } from "@trpc/tanstack-react-query"; import { clearApplicationStorage } from "@utils/clearStorage"; import { logger } from "@utils/logger"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { toast } from "@utils/toast"; +import { useCallback, useEffect, useMemo } from "react"; import { useHotkeys } from "react-hotkeys-hook"; interface GlobalEventHandlersProps { @@ -62,11 +65,6 @@ export function GlobalEventHandlers({ const isWorktreeTask = currentWorkspace?.mode === "worktree"; const { data: allTasks = [] } = useTasks(); - const { invalidateTasks } = useCreateTask(); - const queryClient = useQueryClient(); - const [pendingOpenTaskId, setPendingOpenTaskId] = useState( - null, - ); const sidebarData = useSidebarData({ activeView: view }); const visualTaskOrder = useVisualTaskOrder(sidebarData); @@ -157,40 +155,59 @@ export function GlobalEventHandlers({ log.info("Main access token invalidated for testing"); }, []); - const handleOpenTask = useCallback( - (data?: unknown) => { + const handleCreateTaskFromQuickEntry = useCallback( + async (data?: unknown) => { if (!data || typeof data !== "object") return; - const { taskId } = data as { taskId?: string }; - if (!taskId) return; - // The task may have been created in another window (e.g. quick-entry), - // so the main window's caches for workspaces and folders can be stale - // and miss the new workspace/folder. Invalidate before navigating. - void queryClient.invalidateQueries( - trpcReact.workspace.getAll.pathFilter(), - ); - void queryClient.invalidateQueries( - trpcReact.folders.getFolders.pathFilter(), - ); - const task = taskById.get(taskId); - if (task) { - navigateToTask(task); - } else { - setPendingOpenTaskId(taskId); - invalidateTasks(); + const params = data as { + content: string; + repoPath: string; + workspaceMode: "local" | "worktree"; + branch: string | null; + adapter: "claude" | "codex"; + model: string | null; + reasoningLevel: string | null; + executionMode: string | null; + }; + try { + const taskService = get(RENDERER_TOKENS.TaskService); + const result = await taskService.createTask( + { + content: params.content, + repoPath: params.repoPath, + workspaceMode: params.workspaceMode, + branch: params.branch, + adapter: params.adapter, + model: params.model ?? undefined, + reasoningLevel: params.reasoningLevel ?? undefined, + executionMode: + (params.executionMode as ExecutionMode | null) ?? undefined, + }, + (output) => { + navigateToTask(output.task); + }, + ); + + if (!result.success) { + const log = logger.scope("global-event-handlers"); + log.error("Quick entry task creation failed", { + failedStep: result.failedStep, + error: result.error, + }); + toast.error("Failed to create task", { + description: result.error, + }); + } + } catch (err) { + const log = logger.scope("global-event-handlers"); + log.error("Quick entry task creation threw", { err }); + toast.error("Failed to create task", { + description: err instanceof Error ? err.message : String(err), + }); } }, - [taskById, navigateToTask, invalidateTasks, queryClient, trpcReact], + [navigateToTask], ); - useEffect(() => { - if (!pendingOpenTaskId) return; - const task = taskById.get(pendingOpenTaskId); - if (task) { - navigateToTask(task); - setPendingOpenTaskId(null); - } - }, [pendingOpenTaskId, taskById, navigateToTask]); - const globalOptions = { enableOnFormTags: true, enableOnContentEditable: true, @@ -318,8 +335,8 @@ export function GlobalEventHandlers({ ); useSubscription( - trpcReact.ui.onOpenTask.subscriptionOptions(undefined, { - onData: handleOpenTask, + trpcReact.quickEntry.onCreateTaskRequested.subscriptionOptions(undefined, { + onData: handleCreateTaskFromQuickEntry, }), ); diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx index 3457fd20b..9ca2723e7 100644 --- a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx @@ -13,10 +13,7 @@ import { getCurrentModeFromConfigOptions } from "@features/sessions/stores/sessi import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { ButtonGroup } from "@posthog/quill"; import { Flex, Text } from "@radix-ui/themes"; -import { get } from "@renderer/di/container"; -import { RENDERER_TOKENS } from "@renderer/di/tokens"; import { usePreviewConfig } from "@renderer/features/task-detail/hooks/usePreviewConfig"; -import type { TaskService } from "@renderer/features/task-detail/service/service"; import { trpcClient, useTRPC } from "@renderer/trpc/client"; import { useSubscription } from "@trpc/tanstack-react-query"; import { logger } from "@utils/logger"; @@ -214,11 +211,9 @@ export function QuickEntryView() { const branchForTaskCreation = workspaceMode === "worktree" ? selectedBranch : null; const currentModel = - modelOption?.type === "select" ? modelOption.currentValue : undefined; + modelOption?.type === "select" ? modelOption.currentValue : null; const currentReasoningLevel = - thoughtOption?.type === "select" - ? thoughtOption.currentValue - : undefined; + thoughtOption?.type === "select" ? thoughtOption.currentValue : null; const adapterDefault = adapter === "codex" ? "auto" : "plan"; const modeFallback = defaultInitialTaskMode === "last_used" @@ -229,8 +224,9 @@ export function QuickEntryView() { modeOption ? [modeOption] : undefined, ) ?? modeFallback; - const taskService = get(RENDERER_TOKENS.TaskService); - const result = await taskService.createTask({ + // Hand the request to the main window so it runs the task-creation + // saga in its own renderer context (session store, folder cache, etc.). + await trpcClient.quickEntry.requestCreateTask.mutate({ content: xml, repoPath: selectedDirectory, workspaceMode, @@ -241,18 +237,7 @@ export function QuickEntryView() { executionMode: currentExecutionMode, }); - if (!result.success) { - setError(result.error ?? "Failed to create task"); - log.error("Quick entry task creation failed", { - failedStep: result.failedStep, - error: result.error, - }); - return; - } - - const taskId = result.data.task.id; editor.clear(); - await trpcClient.quickEntry.openTaskInMain.mutate({ taskId }); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(message); From 3f93fe4257d290e44cf3f8a3d34b58bdec24092b Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 22 May 2026 17:06:10 -0300 Subject: [PATCH 7/8] fix(code): invalidate tasks list after quick-entry submit The CreateTaskRequested handler in the main window did not refresh the tasks query, so the new task appeared in the detail view but not in the sidebar until something else triggered a refetch. Call invalidateTasks with the new task inside onTaskReady so it is added optimistically and then refetched. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- apps/code/src/renderer/components/GlobalEventHandlers.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index 2b85c0300..d9555e292 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -6,7 +6,7 @@ import { useSettingsDialogStore } from "@features/settings/stores/settingsDialog import { useSidebarData } from "@features/sidebar/hooks/useSidebarData"; import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder"; import { useSidebarStore } from "@features/sidebar/stores/sidebarStore"; -import { useTasks } from "@features/tasks/hooks/useTasks"; +import { useCreateTask, useTasks } from "@features/tasks/hooks/useTasks"; import { useFocusWorkspace } from "@features/workspace/hooks/useFocusWorkspace"; import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts"; @@ -65,6 +65,7 @@ export function GlobalEventHandlers({ const isWorktreeTask = currentWorkspace?.mode === "worktree"; const { data: allTasks = [] } = useTasks(); + const { invalidateTasks } = useCreateTask(); const sidebarData = useSidebarData({ activeView: view }); const visualTaskOrder = useVisualTaskOrder(sidebarData); @@ -183,6 +184,9 @@ export function GlobalEventHandlers({ (params.executionMode as ExecutionMode | null) ?? undefined, }, (output) => { + // Push the new task into the cached list so the sidebar + // updates immediately, then trigger a refetch. + invalidateTasks(output.task); navigateToTask(output.task); }, ); @@ -205,7 +209,7 @@ export function GlobalEventHandlers({ }); } }, - [navigateToTask], + [navigateToTask, invalidateTasks], ); const globalOptions = { From 3718cbbebab367c07824ce63c89cae791fd9e327 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 22 May 2026 17:23:35 -0300 Subject: [PATCH 8/8] fix(code): address Greptile review on quick entry - Drop the `windowCreated` flag from `QuickEntryService` and call `createQuickEntryWindow` lazily from `show()`. `window.ts` already guards against double-creation; this lets the widget recover if the renderer crashes and the BrowserWindow's `closed` event nulls the module-level handle. Without this, every subsequent toggle silently fails because `createWindow()` was a no-op. - Type `handleCreateTaskFromQuickEntry` against the shared `CreateTaskRequest` schema instead of inline-redeclaring the shape, so schema changes can't silently diverge. - Add a `.catch()` to the `trpcClient.skills.list.query()` call in `QuickEntryView` so a rejected query logs a warning instead of being silently swallowed. Generated-By: PostHog Code Task-Id: 7ff94cd2-f189-4563-a913-a32e48eaae8d --- .../src/main/services/quick-entry/service.ts | 14 +++++++---- .../components/GlobalEventHandlers.tsx | 16 +++--------- .../features/quick-entry/QuickEntryView.tsx | 25 +++++++++++-------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/code/src/main/services/quick-entry/service.ts b/apps/code/src/main/services/quick-entry/service.ts index 769705375..a1056f464 100644 --- a/apps/code/src/main/services/quick-entry/service.ts +++ b/apps/code/src/main/services/quick-entry/service.ts @@ -27,7 +27,6 @@ const SHOW_GRACE_MS = 200; @injectable() export class QuickEntryService extends TypedEventEmitter { private suppressBlurHide = false; - private windowCreated = false; constructor( @inject(MAIN_TOKENS.FoldersService) @@ -36,12 +35,16 @@ export class QuickEntryService extends TypedEventEmitter this.handleBlur(), }); - this.windowCreated = true; + } + + createWindow(): void { + this.ensureWindow(); } private handleBlur(): void { @@ -67,6 +70,8 @@ export class QuickEntryService extends TypedEventEmitter { - if (!data || typeof data !== "object") return; - const params = data as { - content: string; - repoPath: string; - workspaceMode: "local" | "worktree"; - branch: string | null; - adapter: "claude" | "codex"; - model: string | null; - reasoningLevel: string | null; - executionMode: string | null; - }; + async (data?: CreateTaskRequest) => { + if (!data) return; + const params = data; try { const taskService = get(RENDERER_TOKENS.TaskService); const result = await taskService.createTask( diff --git a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx index 9ca2723e7..2a2c25b18 100644 --- a/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx +++ b/apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx @@ -92,16 +92,21 @@ export function QuickEntryView() { // Populate command list for @ file mentions + / skills. useEffect(() => { let cancelled = false; - trpcClient.skills.list.query().then((skills) => { - if (cancelled) return; - useDraftStore.getState().actions.setCommands( - SESSION_ID, - skills.map((s) => ({ - name: s.name, - description: s.description, - })), - ); - }); + trpcClient.skills.list + .query() + .then((skills) => { + if (cancelled) return; + useDraftStore.getState().actions.setCommands( + SESSION_ID, + skills.map((s) => ({ + name: s.name, + description: s.description, + })), + ); + }) + .catch((err) => { + log.warn("Failed to load skills for quick entry", { err }); + }); return () => { cancelled = true; useDraftStore.getState().actions.clearCommands(SESSION_ID);