Skip to content

Context Menu Positioning Lane Chat#682

Merged
arul28 merged 2 commits into
mainfrom
ade/context-menu-positioning-lane-chat-490fc737
Jul 1, 2026
Merged

Context Menu Positioning Lane Chat#682
arul28 merged 2 commits into
mainfrom
ade/context-menu-positioning-lane-chat-490fc737

Conversation

@arul28

@arul28 arul28 commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

ADE   Open in ADE  ·  ade/context-menu-positioning-lane-chat-490fc737 branch  ·  PR #682

Summary by CodeRabbit

  • New Features
    • Added a new “Start chat in lane” action from lane context menus.
    • Starting a chat now opens the Work view with the selected lane preconfigured.
    • Improved context menu positioning across the app so menus stay visible on screen.
  • Bug Fixes
    • Fixed context menus appearing off-screen or partially hidden in terminal, graph, lane, and tab views.

Greptile Summary

This PR adds "Start chat in lane" to lane context menus and fixes context menus appearing off-screen across multiple views (terminal, graph, lane, tab) by replacing ad-hoc layout math with a shared useClampedFixedPosition hook.

  • New useClampedFixedPosition hook measures the rendered element via getBoundingClientRect in a useLayoutEffect, hides the menu with visibility: "hidden" on first render, then reveals it at the correct clamped position — ensuring menus never paint off-screen even once.
  • New "Start chat in lane" action is wired through useStartChatInLane (shared between LanesPage and useWorkLaneContextMenu) and startChatDraftPatch, with unit tests covering both the patch helper and the full navigation flow for remote projects.

Confidence Score: 5/5

Safe to merge — all context menu sites migrate cleanly to the shared hook, and the new chat-in-lane feature is well-guarded and tested.

The positioning refactor is a straightforward extraction with identical semantics (the visibility:hidden pattern is correct and useLayoutEffect fires before paint). The new start-chat feature has unit tests covering the remote-project root path, the patch shape, and the full navigation sequence. No pre-existing guards were removed and no new unguarded code paths were introduced.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hooks/useClampedFixedPosition.ts New shared hook for viewport-clamped fixed positioning; uses useLayoutEffect + visibility:hidden pattern to prevent off-screen flashes. Clean implementation with exported clampFixedPosition for testability.
apps/desktop/src/renderer/hooks/useStartChatInLane.ts New custom hook extracting shared 'start chat in lane' logic; correctly guards against null projectRoot with early return.
apps/desktop/src/renderer/lib/workDraft.ts New helper that returns a typed patch object for opening a chat draft on a specific lane; simple, well-typed, and tested.
apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx Migrated to useClampedFixedPosition and added optional onStartChatInLane menu item behind a conditional guard. Old window.innerHeight arithmetic removed.
apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.tsx Wires useStartChatInLane and passes onStartChatInLane to LaneContextMenu; correctly selects active project root via selectActiveProjectRoot selector.
apps/desktop/src/renderer/components/terminals/SessionContextMenu.tsx Replaced manual useLayoutEffect clamping with useClampedFixedPosition; simplified clampedPosition usage. Logic is equivalent to the previous implementation.
apps/desktop/src/renderer/components/files/v2/ContextMenu.tsx Replaced estimated height arithmetic with useClampedFixedPosition; itemsKey is used as remeasureKey so menu remeasures when items change. Minor padding change (6→8).
apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx Graph context menu now uses useClampedFixedPosition with laneId as remeasureKey; consistent with other menu sites.
apps/desktop/src/renderer/components/app/TabNav.tsx Sidebar context menu migrated to useClampedFixedPosition; anchor is set to null when contextMenu is null so position resets correctly on close.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Right-click on lane / tab / terminal / graph node"] --> B["setState: anchor = {x, y}"]
    B --> C["Render menu div\nvisibility: hidden\nposition = null"]
    C --> D["useLayoutEffect fires\nanchor != null && ref.current != null"]
    D --> E["getBoundingClientRect()\nmeasure actual width/height"]
    E --> F["clampFixedPosition(anchor, size, padding=8)"]
    F --> G["setPosition({left, top})"]
    G --> H["Re-render\nvisibility: visible\nposition = clamped coords"]
    H --> I["Menu displayed within viewport"]

    J["User clicks 'Start chat in lane'"] --> K["onClose() - close menu"]
    K --> L["useStartChatInLane callback"]
    L --> M{projectRoot null?}
    M -- yes --> N["early return (no-op)"]
    M -- no --> O["setWorkViewState with startChatDraftPatch\ndraftKind=chat, draftLaneId, orchestratorEnabled=false"]
    O --> P["selectLane(laneId)"]
    P --> Q["navigate('/work')"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Right-click on lane / tab / terminal / graph node"] --> B["setState: anchor = {x, y}"]
    B --> C["Render menu div\nvisibility: hidden\nposition = null"]
    C --> D["useLayoutEffect fires\nanchor != null && ref.current != null"]
    D --> E["getBoundingClientRect()\nmeasure actual width/height"]
    E --> F["clampFixedPosition(anchor, size, padding=8)"]
    F --> G["setPosition({left, top})"]
    G --> H["Re-render\nvisibility: visible\nposition = clamped coords"]
    H --> I["Menu displayed within viewport"]

    J["User clicks 'Start chat in lane'"] --> K["onClose() - close menu"]
    K --> L["useStartChatInLane callback"]
    L --> M{projectRoot null?}
    M -- yes --> N["early return (no-op)"]
    M -- no --> O["setWorkViewState with startChatDraftPatch\ndraftKind=chat, draftLaneId, orchestratorEnabled=false"]
    O --> P["selectLane(laneId)"]
    P --> Q["navigate('/work')"]
Loading

Reviews (2): Last reviewed commit: "Address review feedback for start-chat a..." | Re-trigger Greptile

Menus now measure and clamp to the viewport so they stay visible in resized windows, and lane right-click menus can open the Work tab new-chat composer with the lane preselected.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 1, 2026 6:40pm

@arul28

arul28 commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@copilot review but do not make fixes

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a shared useClampedFixedPosition hook that clamps popover positioning within the viewport, adopted across TabNav, WorkspaceGraphPage, files ContextMenu, SessionContextMenu, and LaneContextMenu. It also adds a startChatDraftPatch helper and a "Start chat in lane" action wired through LaneContextMenu, LanesPage, and useWorkLaneContextMenu.

Changes

Clamped context menu positioning

Layer / File(s) Summary
Clamping hook and utility
apps/desktop/src/renderer/hooks/useClampedFixedPosition.ts, apps/desktop/src/renderer/hooks/useClampedFixedPosition.test.ts
New clampFixedPosition function and useClampedFixedPosition hook compute and clamp popover left/top within viewport bounds, returning a ref and position; covered by unit tests.
TabNav and WorkspaceGraphPage
apps/desktop/src/renderer/components/app/TabNav.tsx, apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx
Both components adopt the hook to derive clamped left/top for their context menus with a visibility fallback based on raw coordinates.
Files ContextMenu
apps/desktop/src/renderer/components/files/v2/ContextMenu.tsx
Removes manual useRef-based clamping in favor of the hook, deriving itemsKey as a dependency to re-clamp when menu items change.
SessionContextMenu
apps/desktop/src/renderer/components/terminals/SessionContextMenu.tsx
Replaces useLayoutEffect-based state clamping with the hook, adds an early return when no menu, and toggles visibility.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Start chat in lane action

Layer / File(s) Summary
startChatDraftPatch helper
apps/desktop/src/renderer/lib/workDraft.ts, apps/desktop/src/renderer/lib/workDraft.test.ts
New StartChatDraftPatch type and startChatDraftPatch(laneId) function build a chat-draft patch with orchestratorEnabled: false, null item ids, and the given lane id; validated with a test.
LaneContextMenu action and positioning
apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx
Adopts the clamping hook for menu placement, adds an optional onStartChatInLane prop, and renders a "Start chat in lane" action.
LanesPage wiring
apps/desktop/src/renderer/components/lanes/LanesPage.tsx
Adds setWorkViewState and a startChatInLane callback that patches Work view state, selects the lane, and navigates to /work, passed to LaneContextMenu.
useWorkLaneContextMenu wiring and tests
apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.tsx, apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.test.tsx
Adds projectRoot/setWorkViewState store access and a startChatInLane callback wired to LaneContextMenu, with a new test verifying state updates, lane selection, and navigation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: desktop

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title references both main changes: context menu positioning and lane chat actions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/context-menu-positioning-lane-chat-490fc737

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/desktop/src/renderer/hooks/useClampedFixedPosition.ts (1)

30-47: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Spreading deps into the effect's dependency array is fragile.

[anchor?.x, anchor?.y, ...deps] has react-hooks/exhaustive-deps implications: the dependency array's size must stay stable across renders for a given call site. Since deps is an opaque caller-supplied array, nothing in the hook's API prevents a future caller from passing a variable-length array (e.g., conditionally including/excluding an item), which would silently break dependency comparisons. Current callers happen to use fixed-length arrays, but this is baked into the hook's contract.

Consider deriving a single stable key (e.g., deps.join("|")) or documenting/enforcing a fixed-length contract for deps.

♻️ Possible mitigation
 export function useClampedFixedPosition(
   anchor: FixedAnchor | null,
-  deps: unknown[] = [],
+  depsKey: string | number = "",
 ): {
   ref: MutableRefObject<HTMLDivElement | null>;
   position: ClampedFixedPosition | null;
 } {
   const ref = useRef<HTMLDivElement | null>(null);
   const [position, setPosition] = useState<ClampedFixedPosition | null>(null);

   useLayoutEffect(() => {
     if (!anchor || !ref.current) {
       setPosition(null);
       return;
     }
     const rect = ref.current.getBoundingClientRect();
     setPosition(clampFixedPosition(anchor, { width: rect.width, height: rect.height }));
-  }, [anchor?.x, anchor?.y, ...deps]);
+  }, [anchor?.x, anchor?.y, depsKey]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/hooks/useClampedFixedPosition.ts` around lines 30 -
47, The useClampedFixedPosition hook’s effect dependency array is unstable
because it spreads the caller-supplied deps array, which can change length
across renders and break hook comparison semantics. Update the hook to avoid
spreading deps directly in useLayoutEffect; instead derive a single stable
dependency value from deps (or otherwise enforce/document a fixed-length
contract) and keep the effect dependencies shape consistent alongside anchor?.x
and anchor?.y. Locate the fix in useClampedFixedPosition and preserve the
existing clampFixedPosition behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/renderer/components/lanes/LanesPage.tsx`:
- Around line 1282-1295: The start-chat flow is duplicated in LanesPage and
useWorkLaneContextMenu, so extract the shared logic into a common helper/hook
such as useStartChatInLane. Move the current guard, startChatDraftPatch
construction, and selectLane/navigate sequence into that shared function, then
have both call sites pass their own projectRoot, setWorkViewState, selectLane,
and navigate implementations. Keep the existing behavior identical while
centralizing the callback so future changes only happen in one place.

---

Nitpick comments:
In `@apps/desktop/src/renderer/hooks/useClampedFixedPosition.ts`:
- Around line 30-47: The useClampedFixedPosition hook’s effect dependency array
is unstable because it spreads the caller-supplied deps array, which can change
length across renders and break hook comparison semantics. Update the hook to
avoid spreading deps directly in useLayoutEffect; instead derive a single stable
dependency value from deps (or otherwise enforce/document a fixed-length
contract) and keep the effect dependencies shape consistent alongside anchor?.x
and anchor?.y. Locate the fix in useClampedFixedPosition and preserve the
existing clampFixedPosition behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 23c7dcda-64f8-4d04-95e9-751a8a6124a5

📥 Commits

Reviewing files that changed from the base of the PR and between 08a0a48 and 92446cd.

📒 Files selected for processing (12)
  • apps/desktop/src/renderer/components/app/TabNav.tsx
  • apps/desktop/src/renderer/components/files/v2/ContextMenu.tsx
  • apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx
  • apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx
  • apps/desktop/src/renderer/components/lanes/LanesPage.tsx
  • apps/desktop/src/renderer/components/terminals/SessionContextMenu.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.test.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.tsx
  • apps/desktop/src/renderer/hooks/useClampedFixedPosition.test.ts
  • apps/desktop/src/renderer/hooks/useClampedFixedPosition.ts
  • apps/desktop/src/renderer/lib/workDraft.test.ts
  • apps/desktop/src/renderer/lib/workDraft.ts

Comment on lines +1282 to +1295
const startChatInLane = useCallback(
(laneId: string) => {
if (activeProjectRoot) {
setWorkViewState(activeProjectRoot, (prev) => ({
...prev,
...startChatDraftPatch(laneId),
}));
}
selectLane(laneId);
void navigate("/work");
},
[activeProjectRoot, navigate, selectLane, setWorkViewState],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate startChatInLane logic — extract to a shared hook/helper.

This callback is functionally identical to startChatInLane in useWorkLaneContextMenu.tsx (same guard, same patch construction, same selectLane/navigate sequence). Keeping two independent copies risks the two call sites silently diverging as this flow evolves (e.g., future guards against deleting lanes, or telemetry).

Consider extracting a small shared hook (e.g. useStartChatInLane) that both LanesPage and useWorkLaneContextMenu call with their own navigate/selectLane/setWorkViewState/project-root values.

♻️ Proposed shared hook
// apps/desktop/src/renderer/lib/workDraft.ts (or a new hook file)
export function useStartChatInLane(
  projectRoot: string | null,
  setWorkViewState: (projectRoot: string, updater: (prev: WorkProjectViewState) => WorkProjectViewState) => void,
  selectLane: (laneId: string) => void,
  navigate: (path: string) => void | Promise<void>,
) {
  return useCallback(
    (laneId: string) => {
      if (projectRoot) {
        setWorkViewState(projectRoot, (prev) => ({ ...prev, ...startChatDraftPatch(laneId) }));
      }
      selectLane(laneId);
      void navigate("/work");
    },
    [projectRoot, setWorkViewState, selectLane, navigate],
  );
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/lanes/LanesPage.tsx` around lines 1282 -
1295, The start-chat flow is duplicated in LanesPage and useWorkLaneContextMenu,
so extract the shared logic into a common helper/hook such as
useStartChatInLane. Move the current guard, startChatDraftPatch construction,
and selectLane/navigate sequence into that shared function, then have both call
sites pass their own projectRoot, setWorkViewState, selectLane, and navigate
implementations. Keep the existing behavior identical while centralizing the
callback so future changes only happen in one place.

Comment thread apps/desktop/src/renderer/hooks/useClampedFixedPosition.ts Outdated
Comment on lines +63 to +75
const startChatInLane = useCallback(
(laneId: string) => {
if (projectRoot) {
setWorkViewState(projectRoot, (prev) => ({
...prev,
...startChatDraftPatch(laneId),
}));
}
selectLane(laneId);
void navigate("/work");
},
[navigate, projectRoot, selectLane, setWorkViewState],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Silent navigation when projectRoot is null skips draft setup

When projectRoot is null, setWorkViewState is skipped but selectLane and navigate("/work") still execute, taking the user to the Work view without a pre-configured chat draft. The user would see the Work view in its previous state rather than a new chat draft for the selected lane, which is silent unexpected behavior. The same pattern is duplicated in LanesPage.tsx (startChatInLane). Since lanes are only visible with an active project, this state is unlikely in practice, but an early return (or at minimum a guard before the navigate) would make the failure mode explicit.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.tsx
Line: 63-75

Comment:
**Silent navigation when `projectRoot` is null skips draft setup**

When `projectRoot` is null, `setWorkViewState` is skipped but `selectLane` and `navigate("/work")` still execute, taking the user to the Work view without a pre-configured chat draft. The user would see the Work view in its previous state rather than a new chat draft for the selected lane, which is silent unexpected behavior. The same pattern is duplicated in `LanesPage.tsx` (`startChatInLane`). Since lanes are only visible with an active project, this state is unlikely in practice, but an early return (or at minimum a guard before the navigate) would make the failure mode explicit.

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

Fix in Claude Code

Extract useStartChatInLane, guard when no project root, and replace spread deps in useClampedFixedPosition.

Co-authored-by: Cursor <cursoragent@cursor.com>
@arul28

arul28 commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@mintlify

mintlify Bot commented Jul 1, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
ade-ac1c6011 🟢 Ready View Preview Jul 1, 2026, 6:42 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 15484f2156

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@arul28 arul28 merged commit b407a9c into main Jul 1, 2026
29 checks passed
@arul28 arul28 deleted the ade/context-menu-positioning-lane-chat-490fc737 branch July 1, 2026 22:52
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.

1 participant