Skip to content

Conversation

vihar
Copy link
Contributor

@vihar vihar commented Sep 13, 2025

Summary

  • introduce command registry so command palette shortcuts are configurable and extensible
  • refactor command palette and modal to use shared registry with enable hooks and dynamic navigation list
  • format existing command palette helpers

Testing

  • pnpm check (fails: web#check:lint: Unexpected any. Specify a different type)

https://chatgpt.com/codex/tasks/task_e_68c0a5dc9080832a93d2b2f97dbe79f3

Summary by CodeRabbit

  • New Features
    • Two-key command sequences to open projects, cycles, or issues.
    • Multi-step navigation: select a project then jump to cycles or issues, with deep links.
    • Project and cycle selectors plus a generic entity list; issue picker shows recent items, search results, and empty states.
    • Debounced, context-aware search (workspace/project), keyboard flows for sequence, back/esc navigation, and state reset on close.

@Copilot Copilot AI review requested due to automatic review settings September 13, 2025 18:07
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds two-key command sequences and multi-step command-palette flows for projects, cycles, and issues, with new selectors/lists, per-page data handling, store APIs to activate/clear entities, and assorted exports/hooks to wire sequence-driven navigation in the palette.

Changes

Cohort / File(s) Summary
Modal flow & command logic
apps/web/core/components/command-palette/command-modal.tsx
Adds multi-page palette pages (open-project / open-cycle / open-issue), per-page state (recentIssues, issueResults, projectSelectionAction, selectedProjectId), guarded data fetching, search debouncing/results routing, and UI blocks for project/cycle/issue flows with deep links.
Keyboard sequence & wiring
apps/web/core/components/command-palette/command-palette.tsx, apps/web/core/components/command-palette/use-key-sequence.ts, apps/web/core/components/command-palette/commands.ts, apps/web/core/store/base-command-palette.store.ts
Adds two-key sequence support, COMMAND_CONFIG mapping, useKeySequence hook, activation API activateEntity exposed by the store, keySequence handling to trigger entity pages, and clearActiveEntity.
Reusable UI components
apps/web/core/components/command-palette/entity-list.tsx, apps/web/core/components/command-palette/project-selector.tsx, apps/web/core/components/command-palette/cycle-selector.tsx
Adds generic CommandPaletteEntityList and specialized Project/Cycle selectors (headings, empty states, selection callbacks) used by modal pages.
Public re-exports
apps/web/core/components/command-palette/index.ts
Re-exports new modules: project-selector, cycle-selector, entity-list, use-key-sequence, and commands.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CP as Global Key Handler
  participant Store as BaseCommandPaletteStore
  participant Modal as CommandModal
  participant PSel as ProjectSelector
  participant CSel as CycleSelector
  participant ISel as IssueList/Search
  participant Nav as Router

  U->>CP: Type two-key sequence (e.g., "o" then "p")
  CP->>Store: activateEntity("project")
  Store-->>Modal: open palette, activeEntity="project"
  Modal->>PSel: Render project selector
  U->>PSel: Select project (action: navigate|cycle)
  alt action = cycle
    PSel->>Modal: selectedProjectId + action=cycle
    Modal->>CSel: Render cycles for project
    U->>CSel: Select cycle
    CSel->>Nav: Navigate to cycle
  else action = navigate
    PSel->>Modal: selectedProjectId + action=navigate
    Modal->>ISel: Render recent/search issues for project
    U->>ISel: Select issue
    ISel->>Nav: Navigate to issue
  end
  Modal->>Store: clearActiveEntity()
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant Hook as useKeySequence
  participant Modal as CommandModal

  Note over U,Hook: When modal has no active page
  U->>Modal: Press non-mod keys
  Modal->>Hook: onKeyDown events
  Hook->>Hook: Build 2-char sequence (500ms window)
  Hook->>Modal: If matches COMMAND_CONFIG, invoke handler
  Modal->>Modal: Open corresponding entity page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🌐frontend, 🌟improvement, 🛠️refactor

Suggested reviewers

  • sriramveeraghanta
  • prateekshourya29

Poem

A hop, two keys — op, oc, oi delight,
I bound through projects, cycles, issues bright.
Lists and links in tidy rows,
Two-key whispers where the cursor goes.
Thump-thump! 🥕 Palette lit at night.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description contains a brief summary and a testing note about a lint failure but does not follow the repository's required template structure: it omits the exact "### Description" heading format, the "Type of Change" checkbox selections, the "Test Scenarios" details, and the optional "Screenshots and Media" and "References" sections, so required information is incomplete for reviewer and CI expectations. Please update the PR body to match the repository template exactly: add a detailed "### Description" section, check the appropriate "Type of Change" boxes, provide concrete "Test Scenarios" with steps and results, attach screenshots if applicable, and list any related References; also resolve the reported lint error ("Unexpected any") so automated checks can pass.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: generalize command palette entity shortcuts" is concise, single-sentence, and accurately summarizes the primary change (generalizing command-palette entity shortcuts), so it clearly communicates the main intent to reviewers scanning history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/improve-command+k-menu-with-shortcuts-y9px5d

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a command registry system to make command palette shortcuts configurable and extensible. It refactors the existing command palette to use a shared registry with enable hooks and dynamic navigation lists.

  • Introduces CommandPaletteEntity type and command registry system with configurable shortcuts
  • Refactors command palette to support entity-specific selectors (projects, cycles, issues) with dynamic navigation
  • Adds key sequence handling for multi-key shortcuts like "OP" for projects, "OC" for cycles, and "OI" for issues

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/web/core/store/base-command-palette.store.ts Adds entity activation state management for command palette
apps/web/core/components/command-palette/use-key-sequence.ts Implements reusable key sequence detection hook
apps/web/core/components/command-palette/project-selector.tsx Creates project selection component using entity list
apps/web/core/components/command-palette/index.ts Exports new command palette components
apps/web/core/components/command-palette/entity-list.tsx Generic entity list component for command palette items
apps/web/core/components/command-palette/cycle-selector.tsx Creates cycle selection component using entity list
apps/web/core/components/command-palette/commands.ts Defines command configuration registry with shortcuts
apps/web/core/components/command-palette/command-palette.tsx Integrates key sequence handling with command activation
apps/web/core/components/command-palette/command-modal.tsx Major refactor to support entity-specific navigation and selectors

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +165 to +167
.then((res) =>
setRecentIssues(res.map((r: TActivityEntityData) => r.entity_data as TIssueEntityData).slice(0, 10))
)
Copy link
Preview

Copilot AI Sep 13, 2025

Choose a reason for hiding this comment

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

The type assertion as TIssueEntityData could fail if the entity_data doesn't match the expected structure. Consider adding type guards or proper validation before the assertion.

Copilot uses AI. Check for mistakes.

workspaceService
.fetchWorkspaceRecents(workspaceSlug.toString(), "issue")
.then((res) =>
setRecentIssues(res.map((r: TActivityEntityData) => r.entity_data as TIssueEntityData).slice(0, 10))
Copy link
Preview

Copilot AI Sep 13, 2025

Choose a reason for hiding this comment

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

The magic number 10 should be defined as a named constant (e.g., MAX_RECENT_ISSUES = 10) for better maintainability.

Copilot uses AI. Check for mistakes.

Comment on lines +215 to +217
const cycle = getCycleById(cid);
const status = cycle?.status ? cycle.status.toLowerCase() : "";
if (cycle && ["current", "upcoming"].includes(status)) cycles.push(cycle);
Copy link
Preview

Copilot AI Sep 13, 2025

Choose a reason for hiding this comment

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

The hardcoded cycle statuses ['current', 'upcoming'] should be defined as constants or imported from a constants file for better maintainability.

Copilot uses AI. Check for mistakes.

if (page === "open-issue") {
workspaceService
.searchEntity(workspaceSlug.toString(), {
count: 10,
Copy link
Preview

Copilot AI Sep 13, 2025

Choose a reason for hiding this comment

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

The magic number 10 should be defined as a named constant (e.g., MAX_SEARCH_RESULTS = 10) for consistency with other search limits in the codebase.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/core/components/command-palette/command-modal.tsx (1)

286-291: Eliminate as any cast when counting results.

Use Object.values with an array guard to avoid explicit any.

-            const count = Object.keys(results.results).reduce(
-              (accumulator, key) => (results.results as any)[key].length + accumulator,
-              0
-            );
+            const count = Object.values(results.results).reduce(
+              (acc, arr) => acc + (Array.isArray(arr) ? arr.length : 0),
+              0
+            );
🧹 Nitpick comments (12)
apps/web/core/store/base-command-palette.store.ts (2)

88-89: Use ref-tracking for primitive observable.

activeEntity is a primitive union; ref-tracking is cheaper and consistent with other booleans above.

-      activeEntity: observable,
+      activeEntity: observable.ref,

160-166: Clear stale activeEntity when palette closes.

Prevents reopening with an unintended pre-selected entity.

   toggleCommandPaletteModal = (value?: boolean) => {
-    if (value !== undefined) {
-      this.isCommandPaletteOpen = value;
-    } else {
-      this.isCommandPaletteOpen = !this.isCommandPaletteOpen;
-    }
+    if (value !== undefined) this.isCommandPaletteOpen = value;
+    else this.isCommandPaletteOpen = !this.isCommandPaletteOpen;
+    if (!this.isCommandPaletteOpen) this.activeEntity = null;
   };
apps/web/core/components/command-palette/entity-list.tsx (3)

7-15: Require getKey to avoid unstable/duplicate keys.

Using label as a fallback key risks duplicates; make getKey mandatory for stability.

-interface CommandPaletteEntityListProps<T> {
+interface CommandPaletteEntityListProps<T> {
   heading: string;
   items: T[];
   onSelect: (item: T) => void;
-  getKey?: (item: T) => string;
+  getKey: (item: T) => string;
   getLabel: (item: T) => string;
   renderItem?: (item: T) => React.ReactNode;
   emptyText?: string;
 }

31-37: Key derivation and minor typing nit.

Use the now-required getKey and avoid extra getLabel call. Optional: bind item type explicitly in map for clarity.

-      {items.map((item) => (
+      {items.map((item: T) => (
         <Command.Item
-          key={getKey ? getKey(item) : getLabel(item)}
+          key={getKey(item)}
           value={getLabel(item)}
           onSelect={() => onSelect(item)}
           className={cn("focus:outline-none")}
         >

17-25: Alternative: switch to function declaration for better TSX generic inference.

Optional but often reduces generic JSX inference edge-cases.

-export const CommandPaletteEntityList = <T,>({
-  heading,
-  items,
-  onSelect,
-  getKey,
-  getLabel,
-  renderItem,
-  emptyText = "No results found",
-}: CommandPaletteEntityListProps<T>) => {
+export function CommandPaletteEntityList<T>({
+  heading,
+  items,
+  onSelect,
+  getKey,
+  getLabel,
+  renderItem,
+  emptyText = "No results found",
+}: CommandPaletteEntityListProps<T>) {
   if (items.length === 0) return <div className="px-3 py-8 text-center text-sm text-custom-text-300">{emptyText}</div>;
   return (
     <Command.Group heading={heading}>
       {items.map((item) => (
         <Command.Item
           key={getKey ? getKey(item) : getLabel(item)}
           value={getLabel(item)}
           onSelect={() => onSelect(item)}
           className={cn("focus:outline-none")}
         >
           {renderItem ? renderItem(item) : getLabel(item)}
         </Command.Item>
       ))}
     </Command.Group>
   );
-};
+}
apps/web/core/components/command-palette/commands.ts (1)

30-52: Optional: freeze config and keep literal types.

Helps prevent accidental mutation and preserves literal types for id/sequence.

-export const COMMAND_CONFIG: CommandConfig[] = [
+export const COMMAND_CONFIG = [
   {
     id: "open-project",
     sequence: "op",
     title: "Open project...",
     keys: ["O", "P"],
     entity: "project",
   },
   {
     id: "open-cycle",
     sequence: "oc",
     title: "Open cycle...",
     keys: ["O", "C"],
     entity: "cycle",
   },
   {
     id: "open-issue",
     sequence: "oi",
     title: "Open issue...",
     keys: ["O", "I"],
     entity: "issue",
   },
-];
+] as const satisfies readonly CommandConfig[];
apps/web/core/components/command-palette/command-palette.tsx (2)

203-216: Harden 2-key handler and clear timer on match; consider reusing the new hook.

Prevents repeat key churn and stray timeout; also reduces drift vs use-key-sequence.ts.

-      if (!cmdClicked && !altKey && !shiftKey && !isAnyModalOpen) {
+      if (!cmdClicked && !altKey && !shiftKey && !isAnyModalOpen && !e.repeat) {
         keySequence.current = (keySequence.current + keyPressed).slice(-2);
         if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current);
         sequenceTimeout.current = setTimeout(() => {
           keySequence.current = "";
         }, 500);
         const cmd = commandSequenceMap[keySequence.current];
         if (cmd && (!cmd.enabled || cmd.enabled())) {
           e.preventDefault();
           activateEntity(cmd.entity);
           keySequence.current = "";
+          if (sequenceTimeout.current) {
+            clearTimeout(sequenceTimeout.current);
+            sequenceTimeout.current = null;
+          }
           return;
         }
       }

Follow-up: Replace this block with the shared useKeySequence hook to centralize behavior.


167-174: Map build nit: note future dynamic config.

If commands become dynamic, include COMMAND_CONFIG in deps to rebuild safely. No change required now since it’s static.

apps/web/core/components/command-palette/command-modal.tsx (4)

255-304: Guard against stale async updates in debounced search.

Rapid term changes can interleave responses and set stale state. Gate updates with an isActive flag.

   useEffect(() => {
-    if (!workspaceSlug) return;
-
-    setIsLoading(true);
+    if (!workspaceSlug) return;
+    let isActive = true;
+    setIsLoading(true);
@@
-          .then((res) => {
-            setIssueResults(res.issue || []);
-            setResultsCount(res.issue?.length || 0);
-          })
-          .finally(() => {
-            setIsLoading(false);
-            setIsSearching(false);
-          });
+          .then((res) => {
+            if (!isActive) return;
+            setIssueResults(res.issue || []);
+            setResultsCount(res.issue?.length || 0);
+          })
+          .finally(() => {
+            if (!isActive) return;
+            setIsLoading(false);
+            setIsSearching(false);
+          });
@@
-          .then((results) => {
-            setResults(results);
-            const count = Object.keys(results.results).reduce(
-              (accumulator, key) => (results.results as any)[key].length + accumulator,
-              0
-            );
-            setResultsCount(count);
-          })
-          .finally(() => {
-            setIsLoading(false);
-            setIsSearching(false);
-          });
+          .then((results) => {
+            if (!isActive) return;
+            setResults(results);
+            const count = Object.values(results.results).reduce(
+              (acc, arr) => acc + (Array.isArray(arr) ? arr.length : 0),
+              0
+            );
+            setResultsCount(count);
+          })
+          .finally(() => {
+            if (!isActive) return;
+            setIsLoading(false);
+            setIsSearching(false);
+          });
@@
-      setResults(WORKSPACE_DEFAULT_SEARCH_RESULT);
-      setIssueResults([]);
-      setIsLoading(false);
-      setIsSearching(false);
+      setResults(WORKSPACE_DEFAULT_SEARCH_RESULT);
+      setIssueResults([]);
+      setIsLoading(false);
+      setIsSearching(false);
     }
-  }, [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug, page]);
+    return () => {
+      isActive = false;
+    };
+  }, [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug, page]);

81-87: Strongly type pages to prevent string-typo bugs.

Use a union for known pages to get compiler help across the file.

-  const [pages, setPages] = useState<string[]>([]);
+  type Page =
+    | "open-project"
+    | "open-cycle"
+    | "open-issue"
+    | "settings"
+    | "change-issue-state"
+    | "change-issue-priority"
+    | "change-issue-assignee"
+    | "change-interface-theme";
+  const [pages, setPages] = useState<Page[]>([]);

312-317: Redundant state write on close.

if (searchInIssue) setSearchInIssue(true); is a no-op. Remove to avoid confusing state churn.

         onClose={() => {
           closePalette();
-          if (searchInIssue) {
-            setSearchInIssue(true);
-          }
         }}

635-718: Consistent linking for issues (recent vs search).

Good handling of project_identifier vs project__identifier and epics. Consider passing isArchived when source data indicates archived to avoid wrong destinations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3e7cfd and 2b48918.

📒 Files selected for processing (9)
  • apps/web/core/components/command-palette/command-modal.tsx (12 hunks)
  • apps/web/core/components/command-palette/command-palette.tsx (6 hunks)
  • apps/web/core/components/command-palette/commands.ts (1 hunks)
  • apps/web/core/components/command-palette/cycle-selector.tsx (1 hunks)
  • apps/web/core/components/command-palette/entity-list.tsx (1 hunks)
  • apps/web/core/components/command-palette/index.ts (1 hunks)
  • apps/web/core/components/command-palette/project-selector.tsx (1 hunks)
  • apps/web/core/components/command-palette/use-key-sequence.ts (1 hunks)
  • apps/web/core/store/base-command-palette.store.ts (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/web/core/components/command-palette/commands.ts (1)
apps/web/core/store/base-command-palette.store.ts (1)
  • CommandPaletteEntity (11-11)
apps/web/core/components/command-palette/cycle-selector.tsx (1)
apps/web/core/components/command-palette/entity-list.tsx (1)
  • CommandPaletteEntityList (17-42)
apps/web/core/components/command-palette/project-selector.tsx (1)
apps/web/core/components/command-palette/entity-list.tsx (1)
  • CommandPaletteEntityList (17-42)
apps/web/core/components/command-palette/command-modal.tsx (8)
apps/space/core/hooks/store/use-cycle.ts (1)
  • useCycle (7-11)
apps/web/core/components/command-palette/commands.ts (1)
  • COMMAND_CONFIG (30-52)
apps/web/core/components/command-palette/use-key-sequence.ts (1)
  • useKeySequence (5-25)
apps/web/core/store/project/project.store.ts (1)
  • joinedProjectIds (225-236)
apps/web/core/components/command-palette/project-selector.tsx (1)
  • CommandPaletteProjectSelector (12-21)
apps/web/core/components/command-palette/cycle-selector.tsx (1)
  • CommandPaletteCycleSelector (12-21)
apps/web/core/components/command-palette/entity-list.tsx (1)
  • CommandPaletteEntityList (17-42)
packages/utils/src/work-item/base.ts (1)
  • generateWorkItemLink (335-357)
apps/web/core/components/command-palette/command-palette.tsx (1)
apps/web/core/components/command-palette/commands.ts (2)
  • CommandConfig (3-28)
  • COMMAND_CONFIG (30-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Analyze (python)
🔇 Additional comments (5)
apps/web/core/components/command-palette/cycle-selector.tsx (1)

12-21: LGTM — simple, typed wrapper looks good.

No issues spotted. Mirrors project selector consistently.

apps/web/core/components/command-palette/index.ts (1)

5-9: LGTM — re-exports are cohesive.

Surface area matches the new modules.

apps/web/core/components/command-palette/project-selector.tsx (1)

12-21: LGTM — mirrors cycle selector and typed correctly.

Consistent props and empty state.

apps/web/core/components/command-palette/command-modal.tsx (2)

171-189: Nice abstraction: registry-driven sequence handlers.

Mapping config to handlers with useMemo keeps the palette extensible and side-effect free at render.


606-624: Confirm navigation target for project route.

Using /${workspaceSlug}/projects/${project.id}/issues assumes the issues index route exists and is canonical. Verify or adjust to the current project default tab.

Comment on lines 349 to +354
onKeyDown={(e: any) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
const key = e.key.toLowerCase();
if (!e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey && !page && searchTerm === "") {
handleKeySequence(e);
}
if ((e.metaKey || e.ctrlKey) && key === "k") {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix lint failure: remove explicit any on keyboard event.

Type the event as React.KeyboardEvent to satisfy no-explicit-any and align with useKeySequence.

Apply:

-                    onKeyDown={(e: any) => {
+                    onKeyDown={(e: React.KeyboardEvent) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onKeyDown={(e: any) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
const key = e.key.toLowerCase();
if (!e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey && !page && searchTerm === "") {
handleKeySequence(e);
}
if ((e.metaKey || e.ctrlKey) && key === "k") {
onKeyDown={(e: React.KeyboardEvent) => {
const key = e.key.toLowerCase();
if (!e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey && !page && searchTerm === "") {
handleKeySequence(e);
}
if ((e.metaKey || e.ctrlKey) && key === "k") {
🤖 Prompt for AI Agents
In apps/web/core/components/command-palette/command-modal.tsx around lines 349
to 354, the onKeyDown handler types the event as `any`; replace that with
`React.KeyboardEvent` (or `React.KeyboardEvent<HTMLElement>` if you want a
specific element) to satisfy the no-explicit-any lint rule and match
useKeySequence typing, and ensure React types are available (import React or
import type { KeyboardEvent } from 'react' / use the appropriate local React
import if required).

Comment on lines +350 to +353
const key = e.key.toLowerCase();
if (!e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey && !page && searchTerm === "") {
handleKeySequence(e);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Quick check script to ensure no any remains.

Run to confirm the lint error is resolved in this file.

Also applies to: 286-291


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -nP ':\s*any\b|as any\b' apps/web/core/components/command-palette/command-modal.tsx || echo "No explicit any found in command-modal.tsx"

Length of output: 140


Remove remaining any usages in command-modal.tsx

rg shows two explicit any usages that still cause lint errors — fix them:

  • apps/web/core/components/command-palette/command-modal.tsx:288 — replace (results.results as any)[key] with a properly typed access (e.g., type results.results as Record<string, SearchResult[]> or iterate Object.values(results.results)), avoid as any.
  • apps/web/core/components/command-palette/command-modal.tsx:349 — replace onKeyDown={(e: any) => ...} with a correctly typed event parameter (e.g., React.KeyboardEvent<HTMLElement> or the specific element type).

Re-run the quick check: rg -nP ':\s*any\b|as any\b' apps/web/core/components/command-palette/command-modal.tsx.

🤖 Prompt for AI Agents
In apps/web/core/components/command-palette/command-modal.tsx around lines 288
and 349, remove the remaining any usages: replace the cast (results.results as
any)[key] by giving results.results a proper type (e.g., declare results.results
as Record<string, SearchResult[]> or iterate with Object.values(results.results)
to avoid indexing with any), and change onKeyDown={(e: any) => ...} to use the
correct React event type (e.g., onKeyDown={(e: React.KeyboardEvent<HTMLElement>)
=> ...} or the specific element type like React.KeyboardEvent<HTMLDivElement>);
after making these changes re-run the quick check rg -nP ':\s*any\b|as any\b'
apps/web/core/components/command-palette/command-modal.tsx to ensure no any
remains.

Comment on lines +19 to 20
import type { CommandPaletteEntity } from "@/store/base-command-palette.store";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unused type import.

Avoids lint noise.

-import type { CommandPaletteEntity } from "@/store/base-command-palette.store";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { CommandPaletteEntity } from "@/store/base-command-palette.store";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
🤖 Prompt for AI Agents
In apps/web/core/components/command-palette/command-palette.tsx around lines 19
to 20, the imported type CommandPaletteEntity is not used and triggers lint
noise; remove the unused type-only import (or delete it from the import list) so
the file no longer imports CommandPaletteEntity, then run lint/TS check to
confirm no remaining references.

Comment on lines +164 to +166
const keySequence = useRef("");
const sequenceTimeout = useRef<NodeJS.Timeout | null>(null);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use browser-safe timer type.

Aligns with DOM setTimeout return type.

-  const sequenceTimeout = useRef<NodeJS.Timeout | null>(null);
+  const sequenceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const keySequence = useRef("");
const sequenceTimeout = useRef<NodeJS.Timeout | null>(null);
const keySequence = useRef("");
const sequenceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
🤖 Prompt for AI Agents
In apps/web/core/components/command-palette/command-palette.tsx around lines 164
to 166, the ref sequenceTimeout is typed as NodeJS.Timeout which is a
Node-specific type; change it to the browser-safe timer type (e.g. number | null
or ReturnType<typeof setTimeout>) so it matches DOM setTimeout return value.
Update the ref declaration to use that type and ensure any clearTimeout calls
remain compatible (no runtime changes needed), and remove any Node-specific type
imports if present.

renderItem,
emptyText = "No results found",
}: CommandPaletteEntityListProps<T>) => {
if (items.length === 0) return <div className="px-3 py-8 text-center text-sm text-custom-text-300">{emptyText}</div>;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Locate the lint “Unexpected any” offender.

If ESLint is flagging this area, it’s likely from generic inference at JSX call sites. Let’s confirm the exact file/line.


🏁 Script executed:

#!/bin/bash
# Find explicit 'any' in the affected surfaces
rg -nP '\b(any|as any|<any>)\b' apps/web/core/components/command-palette apps/web/core/store | sed 's/^/ANY: /'
# Also flag NodeJS.Timeout usages in web code
rg -nP 'NodeJS\.Timeout' apps/web/core/components/command-palette apps/web/core/store | sed 's/^/TIMEOUT: /'

Length of output: 5539


'Unexpected any' isn't coming from entity-list.tsx — explicit any found elsewhere

No any in apps/web/core/components/command-palette/entity-list.tsx; search found explicit any and NodeJS.Timeout usages you must fix:

  • apps/web/core/components/command-palette/command-modal.tsx:288 — (results.results as any)[key]...
  • apps/web/core/components/command-palette/command-modal.tsx:349 — onKeyDown={(e: any) => ...}
  • apps/web/core/components/command-palette/actions/search-results.tsx:32 — (results.results as any)[key]
  • apps/web/core/components/command-palette/actions/search-results.tsx:38 — section.map((item: any) => ...)
  • apps/web/core/components/command-palette/use-key-sequence.ts:7 — useRef<NodeJS.Timeout | null>
  • apps/web/core/components/command-palette/command-palette.tsx:165 — useRef<NodeJS.Timeout | null>

Fixes: define proper types for search results/items (avoid (results as any)), type event handlers (e.g., React.KeyboardEvent), and replace NodeJS.Timeout with number or ReturnType<typeof setTimeout> for browser code.

Comment on lines +5 to +25
export const useKeySequence = (handlers: Record<string, () => void>, timeout = 500) => {
const sequence = useRef("");
const sequenceTimeout = useRef<NodeJS.Timeout | null>(null);

return (e: React.KeyboardEvent) => {
const key = e.key.toLowerCase();
sequence.current = (sequence.current + key).slice(-2);

if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current);
sequenceTimeout.current = setTimeout(() => {
sequence.current = "";
}, timeout);

const action = handlers[sequence.current];
if (action) {
e.preventDefault();
action();
sequence.current = "";
}
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix browser timer type, ignore modifier/non-char keys, and clear timer on hit.

Prevents type noise in web envs, avoids accidental matches (e.g., Arrow keys), and stops a dangling timeout after a match.

-import { useRef } from "react";
+import { useRef } from "react";
+import type React from "react";

 export const useKeySequence = (handlers: Record<string, () => void>, timeout = 500) => {
   const sequence = useRef("");
-  const sequenceTimeout = useRef<NodeJS.Timeout | null>(null);
+  const sequenceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);

-  return (e: React.KeyboardEvent) => {
-    const key = e.key.toLowerCase();
+  return (e: React.KeyboardEvent) => {
+    if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey || e.repeat) return;
+    const keyRaw = e.key;
+    if (!keyRaw || keyRaw.length !== 1) return; // ignore non-character keys
+    const key = keyRaw.toLowerCase();
     sequence.current = (sequence.current + key).slice(-2);

-    if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current);
+    if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current);
     sequenceTimeout.current = setTimeout(() => {
       sequence.current = "";
     }, timeout);

     const action = handlers[sequence.current];
     if (action) {
       e.preventDefault();
       action();
       sequence.current = "";
+      if (sequenceTimeout.current) {
+        clearTimeout(sequenceTimeout.current);
+        sequenceTimeout.current = null;
+      }
     }
   };
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useKeySequence = (handlers: Record<string, () => void>, timeout = 500) => {
const sequence = useRef("");
const sequenceTimeout = useRef<NodeJS.Timeout | null>(null);
return (e: React.KeyboardEvent) => {
const key = e.key.toLowerCase();
sequence.current = (sequence.current + key).slice(-2);
if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current);
sequenceTimeout.current = setTimeout(() => {
sequence.current = "";
}, timeout);
const action = handlers[sequence.current];
if (action) {
e.preventDefault();
action();
sequence.current = "";
}
};
};
import { useRef } from "react";
import type React from "react";
export const useKeySequence = (handlers: Record<string, () => void>, timeout = 500) => {
const sequence = useRef("");
const sequenceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
return (e: React.KeyboardEvent) => {
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey || e.repeat) return;
const keyRaw = e.key;
if (!keyRaw || keyRaw.length !== 1) return; // ignore non-character keys
const key = keyRaw.toLowerCase();
sequence.current = (sequence.current + key).slice(-2);
if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current);
sequenceTimeout.current = setTimeout(() => {
sequence.current = "";
}, timeout);
const action = handlers[sequence.current];
if (action) {
e.preventDefault();
action();
sequence.current = "";
if (sequenceTimeout.current) {
clearTimeout(sequenceTimeout.current);
sequenceTimeout.current = null;
}
}
};
};
🤖 Prompt for AI Agents
In apps/web/core/components/command-palette/use-key-sequence.ts around lines 5
to 25, the hook currently uses NodeJS.Timeout (server type), listens to all keys
(including modifiers and non-character keys like Arrow/Shift), and leaves a
pending timeout after a match; change the timeout ref type to number | null for
browser setTimeout, ignore keys that are modifiers or non-printable (e.g., check
e.key length > 1 or match against a whitelist/regex of single-char keys and
return early for others), convert the key to lower-case only for printable
chars, and when a handler matches clear the active timeout (clearTimeout and set
ref to null) before invoking the action and resetting sequence.current to "" to
avoid dangling timers and accidental matches.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/core/components/command-palette/command-modal.tsx (1)

31-41: Fix duplicate imports from '@/components/command-palette' (lint blocker).

Combine into a single import to satisfy no-duplicate-imports and unblock CI.

Apply:

 import {
   ChangeIssueAssignee,
   ChangeIssuePriority,
   ChangeIssueState,
   CommandPaletteHelpActions,
   CommandPaletteIssueActions,
   CommandPaletteProjectActions,
   CommandPaletteSearchResults,
   CommandPaletteThemeActions,
   CommandPaletteWorkspaceSettingsActions,
-} from "@/components/command-palette";
+  CommandPaletteProjectSelector,
+  CommandPaletteCycleSelector,
+  CommandPaletteEntityList,
+  useKeySequence,
+  COMMAND_CONFIG,
+} from "@/components/command-palette";
@@
-import {
-  CommandPaletteProjectSelector,
-  CommandPaletteCycleSelector,
-  CommandPaletteEntityList,
-  useKeySequence,
-} from "@/components/command-palette";
-
-import { COMMAND_CONFIG } from "@/components/command-palette";

Also applies to: 43-49

♻️ Duplicate comments (4)
apps/web/core/components/command-palette/command-modal.tsx (4)

289-295: Eliminate as any when counting results (lint blocker).

Use Object.values with an array guard.

Apply:

-            const count = Object.keys(results.results).reduce(
-              (accumulator, key) => (results.results as any)[key].length + accumulator,
-              0
-            );
+            const count = Object.values(results.results).reduce<number>(
+              (acc, value) => acc + (Array.isArray(value) ? value.length : 0),
+              0
+            );

219-221: Avoid hardcoded cycle statuses. Extract a constant.

Apply:

-        if (cycle && ["current", "upcoming"].includes(status)) cycles.push(cycle);
+        const VISIBLE_CYCLE_STATUSES = ["current", "upcoming"] as const;
+        if (cycle && VISIBLE_CYCLE_STATUSES.includes(status as (typeof VISIBLE_CYCLE_STATUSES)[number])) {
+          cycles.push(cycle);
+        }

68-70: Replace magic numbers with named constants.

Apply:

 const workspaceService = new WorkspaceService();
 
+const MAX_RECENT_ISSUES = 10;
+const MAX_SEARCH_RESULTS = 10;
-        setRecentIssues(res.map((r: TActivityEntityData) => r.entity_data as TIssueEntityData).slice(0, 10))
+        setRecentIssues(res.map((r: TActivityEntityData) => r.entity_data as TIssueEntityData).slice(0, MAX_RECENT_ISSUES))
-            count: 10,
+            count: MAX_SEARCH_RESULTS,

Also applies to: 167-169, 268-269


352-357: Type the keyboard event to remove explicit any (lint blocker).

Apply:

-                    onKeyDown={(e: any) => {
+                    onKeyDown={(e: React.KeyboardEvent<HTMLElement>) => {
                       const key = e.key.toLowerCase();
                       if (!e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey && !page && searchTerm === "") {
                         handleKeySequence(e);
                       }
🧹 Nitpick comments (2)
apps/web/core/components/command-palette/cycle-selector.tsx (1)

16-18: Guard label against nullable names.

If cycle.name can be empty/undefined, Command.Item value should still be a string.

Apply:

-    getLabel={(cycle) => cycle.name}
+    getLabel={(cycle) => cycle.name ?? "Untitled cycle"}
apps/web/core/components/command-palette/command-modal.tsx (1)

3-3: Remove unused useRef import (lint).

Apply:

-import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
+import React, { useEffect, useState, useMemo, useCallback } from "react";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b48918 and b5086d7.

📒 Files selected for processing (2)
  • apps/web/core/components/command-palette/command-modal.tsx (13 hunks)
  • apps/web/core/components/command-palette/cycle-selector.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/core/components/command-palette/command-modal.tsx (9)
apps/space/core/hooks/store/use-cycle.ts (1)
  • useCycle (7-11)
apps/web/core/store/base-command-palette.store.ts (1)
  • CommandPaletteEntity (11-11)
apps/web/core/components/command-palette/commands.ts (1)
  • COMMAND_CONFIG (30-52)
apps/web/core/components/command-palette/use-key-sequence.ts (1)
  • useKeySequence (5-25)
apps/web/core/store/project/project.store.ts (1)
  • joinedProjectIds (225-236)
apps/web/core/components/command-palette/project-selector.tsx (1)
  • CommandPaletteProjectSelector (12-21)
apps/web/core/components/command-palette/cycle-selector.tsx (1)
  • CommandPaletteCycleSelector (12-21)
apps/web/core/components/command-palette/entity-list.tsx (1)
  • CommandPaletteEntityList (17-42)
packages/utils/src/work-item/base.ts (1)
  • generateWorkItemLink (335-357)
apps/web/core/components/command-palette/cycle-selector.tsx (1)
apps/web/core/components/command-palette/entity-list.tsx (1)
  • CommandPaletteEntityList (17-42)
🪛 GitHub Check: Build and lint web apps
apps/web/core/components/command-palette/command-modal.tsx

[failure] 49-49:
'@/components/command-palette' import is duplicated


[failure] 43-43:
'@/components/command-palette' import is duplicated

🪛 GitHub Actions: Build and lint web apps
apps/web/core/components/command-palette/command-modal.tsx

[error] 43-43: ESLint: no-duplicate-imports. Duplicate import from '@/components/command-palette' in command-modal.tsx. Failing step: eslint . --max-warnings 821 during web:check:lint.


[warning] 3-3: React: 'useRef' is defined but never used.

🔇 Additional comments (1)
apps/web/core/components/command-palette/cycle-selector.tsx (1)

12-21: LGTM — simple, typed wrapper over EntityList looks correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant