Skip to content

feat(code): add Option+Space quick entry widget#2307

Open
fercgomes wants to merge 8 commits into
mainfrom
posthog-code/quick-entry-widget
Open

feat(code): add Option+Space quick entry widget#2307
fercgomes wants to merge 8 commits into
mainfrom
posthog-code/quick-entry-widget

Conversation

@fercgomes
Copy link
Copy Markdown
Contributor

@fercgomes fercgomes commented May 22, 2026

Summary

Claude-Desktop-style quick entry: 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.

Screen.Recording.2026-05-22.at.17.09.04.mov
  • New QuickEntryService + quickEntry tRPC router orchestrate show/hide/submit and getRecentRepos.
  • Frameless, transparent, always-on-top BrowserWindow (720×132) loaded from the same renderer bundle with hash #quick-entry; renderer entry branches to mount QuickEntryRoot.
  • 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`.
  • Adds OpenTask event on `UIService` so the QE submit flow can tell the main renderer to navigate to a specific task.
  • Blur-to-hide with a 120ms grace so dropdown popups don't dismiss the widget.

Architectural note: because main-process services can't import `electron` directly (biome `noRestrictedImports` rule), the BrowserWindow primitives live in `window.ts` and the service calls them.

Out of scope / follow-ups:

  • Settings UI toggle to enable/disable the widget.
  • Customizable hotkey.
  • True double-tap-Option detection (would require `uiohook-napi` + macOS Accessibility permission).
  • Windows/Linux polish (`Alt+Space` collides with the window menu on those platforms).

Test plan

  • `pnpm dev`, app boots, main window appears.
  • With main window focused, press Option+Space → widget appears bottom-center of the active display.
  • Minimize the main window, press Option+Space from another app (e.g. Chrome) → widget appears in front.
  • Type a prompt, pick a folder from the dropdown, hit Enter → widget hides, main window restores + focuses, navigates to the new task detail, agent starts.
  • Press Option+Space to open, then press Escape → widget hides.
  • Press Option+Space to open, click another app → blur fires → widget hides.
  • Press Option+Space twice rapidly → second press toggles off.
  • Quit the app (`Cmd+Q`), reopen → shortcut still registers.
  • `pnpm --filter code typecheck` passes.
  • Biome clean on changed files.

Screenshots

The widget mimics the layout shown in the original screenshot from Claude Desktop: a horizontal pill at the bottom of the screen with prompt input + repo dropdown + send button.

fercgomes added 7 commits May 22, 2026 15:54
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
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
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
- 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
…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
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
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
@fercgomes fercgomes marked this pull request as ready for review May 22, 2026 20:10
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Comments Outside Diff (2)

  1. apps/code/src/main/services/quick-entry/service.ts, line 172-178 (link)

    P1 windowCreated flag can go stale after a renderer crash

    createWindow() sets this.windowCreated = true and never clears it unless dispose() is called. If the BrowserWindow closes unexpectedly (renderer process crash or OOM kill) without dispose() being invoked, the "closed" event in window.ts nulls out quickEntryWindow, but windowCreated stays true. From that point on, createWindow() is a no-op, so every subsequent toggle() call falls into show()showQuickEntryWindow() → logs a warning and returns false. The widget is permanently broken until the app restarts with no user-facing indication.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/code/src/main/services/quick-entry/service.ts
    Line: 172-178
    
    Comment:
    **`windowCreated` flag can go stale after a renderer crash**
    
    `createWindow()` sets `this.windowCreated = true` and never clears it unless `dispose()` is called. If the BrowserWindow closes unexpectedly (renderer process crash or OOM kill) without `dispose()` being invoked, the `"closed"` event in `window.ts` nulls out `quickEntryWindow`, but `windowCreated` stays `true`. From that point on, `createWindow()` is a no-op, so every subsequent `toggle()` call falls into `show()``showQuickEntryWindow()` → logs a warning and returns `false`. The widget is permanently broken until the app restarts with no user-facing indication.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/code/src/renderer/components/GlobalEventHandlers.tsx, line 578-632 (link)

    P2 Inline type re-declaration duplicates CreateTaskRequest from schemas.ts

    The params object shape is declared inline via data as { content: string; repoPath: string; … }, which exactly replicates the CreateTaskRequest interface already defined in apps/code/src/main/services/quick-entry/schemas.ts. If a field is added or renamed in the schema it must also be updated here, creating a silent divergence risk. The tRPC subscription for onCreateTaskRequested already exposes the inferred type, so handleCreateTaskFromQuickEntry can accept the properly-typed argument directly instead of data?: unknown with a subsequent cast.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/code/src/renderer/components/GlobalEventHandlers.tsx
    Line: 578-632
    
    Comment:
    **Inline type re-declaration duplicates `CreateTaskRequest` from `schemas.ts`**
    
    The `params` object shape is declared inline via `data as { content: string; repoPath: string; … }`, which exactly replicates the `CreateTaskRequest` interface already defined in `apps/code/src/main/services/quick-entry/schemas.ts`. If a field is added or renamed in the schema it must also be updated here, creating a silent divergence risk. The tRPC subscription for `onCreateTaskRequested` already exposes the inferred type, so `handleCreateTaskFromQuickEntry` can accept the properly-typed argument directly instead of `data?: unknown` with a subsequent cast.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/code/src/main/services/quick-entry/service.ts:172-178
**`windowCreated` flag can go stale after a renderer crash**

`createWindow()` sets `this.windowCreated = true` and never clears it unless `dispose()` is called. If the BrowserWindow closes unexpectedly (renderer process crash or OOM kill) without `dispose()` being invoked, the `"closed"` event in `window.ts` nulls out `quickEntryWindow`, but `windowCreated` stays `true`. From that point on, `createWindow()` is a no-op, so every subsequent `toggle()` call falls into `show()``showQuickEntryWindow()` → logs a warning and returns `false`. The widget is permanently broken until the app restarts with no user-facing indication.

### Issue 2 of 3
apps/code/src/renderer/components/GlobalEventHandlers.tsx:578-632
**Inline type re-declaration duplicates `CreateTaskRequest` from `schemas.ts`**

The `params` object shape is declared inline via `data as { content: string; repoPath: string; … }`, which exactly replicates the `CreateTaskRequest` interface already defined in `apps/code/src/main/services/quick-entry/schemas.ts`. If a field is added or renamed in the schema it must also be updated here, creating a silent divergence risk. The tRPC subscription for `onCreateTaskRequested` already exposes the inferred type, so `handleCreateTaskFromQuickEntry` can accept the properly-typed argument directly instead of `data?: unknown` with a subsequent cast.

### Issue 3 of 3
apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx:95-104
Missing `.catch()` on the skills query — if the call rejects, it silently swallows the error, leaving the command list unpopulated with no feedback.

```suggestion
    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 });
    });
```

Reviews (1): Last reviewed commit: "fix(code): invalidate tasks list after q..." | Re-trigger Greptile

Comment on lines +95 to +104
trpcClient.skills.list.query().then((skills) => {
if (cancelled) return;
useDraftStore.getState().actions.setCommands(
SESSION_ID,
skills.map((s) => ({
name: s.name,
description: s.description,
})),
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing .catch() on the skills query — if the call rejects, it silently swallows the error, leaving the command list unpopulated with no feedback.

Suggested change
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 });
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/quick-entry/QuickEntryView.tsx
Line: 95-104

Comment:
Missing `.catch()` on the skills query — if the call rejects, it silently swallows the error, leaving the command list unpopulated with no feedback.

```suggestion
    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 });
    });
```

How can I resolve this? If you propose a fix, please make it concise.

@fercgomes fercgomes requested a review from a team May 22, 2026 20:17
- 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
@adamleithp
Copy link
Copy Markdown
Contributor

this is awesome! I really suggest you add the toggle on/off in settings before pushing this lol

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants