Skip to content

Desktop UI cleanup and Linear issue resolve flows#368

Merged
arul28 merged 3 commits into
mainfrom
ade/ui-clean-up-3470f34e
May 27, 2026
Merged

Desktop UI cleanup and Linear issue resolve flows#368
arul28 merged 3 commits into
mainfrom
ade/ui-clean-up-3470f34e

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 26, 2026

Summary

  • Simplify control flow across desktop services: replace if-else chains with lookup tables, extract shared helpers (readString, resolveActivityUpdateToken, tokenTypeDetectionLabel, conflictDescription), DRY repeated patterns (heading attrs, APNS topic, connection status dot, commit graph colors)
  • Fix Linear issueUpdate mutation to use addedLabelIds instead of labelIds (was replacing all labels instead of appending)
  • Add "duplicate" to Linear state group ordering and use hasActiveFilters() for filter comparison
  • Remove redundant CSSProperties inline styles from TaskCard (already handled by Tailwind line-clamp-3)
  • Update tests to match new UI: drawer-based proof/handoff access, terminal button naming, PR button in git toolbar, permission mode label casing, work session filtering
  • Align docs with orchestration rename (missions → orchestration) and add orchestration IPC/renderer sections

Test plan

  • Verify Linear label assignment doesn't clobber existing labels
  • Check proof drawer opens via chat actions drawer
  • Confirm handoff flow works through drawer menu
  • Verify terminal toggle button works in RunPage
  • Check commit graph renders correctly with simplified color logic

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added browser-based development preview for desktop app with live runtime bridge support
    • Unified chat actions drawer combining agents, proof verification, and handoff workflows
    • Linear issue resolution workflows: create lanes or resolve issues in new/existing lanes directly from issue browser
    • Lane colors now automatically persist and allocate when creating or importing lanes
    • Grouped Linear issue browser by state with collapsible sections and improved filtering
  • UI/UX Improvements

    • Redesigned header with connection status indicators for mobile sync and remote integration
    • New GlowMenu component for navigation with animated effects
    • Custom VCS icons (branch and lane icons) replacing generic symbols
    • Compact layout options for various components and sidebar tools
    • Simplified visual effects in chat composer
  • Integrations

    • Enhanced Linear project support with icon and color field handling
    • Improved project list rendering with optimized page sizes
    • Better label management in Linear issues

Review Change Stack

Greptile Summary

This PR refactors the desktop UI with control-flow simplifications (lookup tables, shared helpers), adds Linear issue resolve workflows (create lane, resolve in new/existing lane), introduces automatic lane color allocation, and fixes the Linear label mutation to use addedLabelIds instead of labelIds. It also adds a browser-based dev preview runtime bridge and consolidates the proof/handoff/agents UI into a single ChatActionsDrawerPanel.

  • Linear label fix: issueUpdate now uses addedLabelIds so existing labels are appended rather than replaced.
  • Lane color auto-allocation: new allocateLaneColor helper in shared/laneColorPalette.ts is wired into four laneService insert paths and is project-scoped via the per-project DB.
  • Linear resolve flows: new LinearIssueResolveModals + linearIssueWorkNavigation module drives "create lane", "resolve in new lane", and "resolve in existing lane" workflows with a 60-second TTL on the pending context singleton.
  • Code cleanup: filterStatus filter logic removed from filteredSessions (UI control was already removed), proofDrawerOpen migrated to chatActionsOpen/Tab with legacy migration, and ChatGitToolbar stripped of inline commit/diff UI.

Confidence Score: 5/5

Safe to merge — the addedLabelIds fix is correct, lane color allocation is project-scoped, and the new pending context TTL addresses the stale-context scenario from the prior review.

All substantive logic changes (label mutation fix, color auto-allocation SQL inserts, pending-context TTL) are straightforward and well-contained. The one new finding is a cosmetic rendering edge case in LinearProjectIcon where an absent project color produces an invalid CSS background on the letter chip; it is visible only for Linear projects with no color configured and does not affect data correctness or navigation.

apps/desktop/src/renderer/components/lanes/linearProjectIcon.tsx — the non-glyph letter chip uses ?? instead of || for its background fallback.

Important Files Changed

Filename Overview
apps/desktop/src/main/services/cto/linearClient.ts Fixes label mutation to use addedLabelIds, adds icon/color fields to project query, and bumps project page size to 50.
apps/desktop/src/renderer/components/app/LinearIssueResolveModals.tsx New file — three modals for lane/chat resolve flows; ModalFooter disabled state is only tied to busy, not to invalid form state (previously flagged).
apps/desktop/src/renderer/components/lanes/linearProjectIcon.tsx New file — renders Linear project icons from shortcodes/emoji; non-glyph fallback uses color ?? PRIMARY which passes empty-string through, leaving letter chip without a background.
apps/desktop/src/renderer/lib/linearIssueWorkNavigation.ts New module — pending context singleton for Linear issue work flows; addresses stale context concern from previous review with a 60-second TTL.
apps/desktop/src/main/services/lanes/laneService.ts Auto-allocates lane colors on all four insert paths using project-scoped getAllLaneRows; SQL parameter order updated correctly in each INSERT.
apps/desktop/src/shared/laneColorPalette.ts New shared module extracted from renderer; adds allocateLaneColor (first available or random) and randomLaneColor helpers.
apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx Redesigned with state-grouped issue list, static STATE_TABS replacing dynamic stateOptions, and hasActiveFilters helper; createdAt-first date display noted in prior review.
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx Migrates proofDrawerOpen to chatActionsOpen/Tab with legacy session-storage migration; adds initialModelId and initialLinearIssueContextSource props.
apps/desktop/src/renderer/components/terminals/useWorkSessions.ts filterStatus removed from filteredSessions logic — intentional cleanup matching SessionListPane UI removal; setFilterStatus still exported for API compatibility.
apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx Refactored to support icon/menu-row variants and wire resolve modals; handleResolveInExistingLane async/sync flicker noted in prior review.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[LinearQuickViewButton] -->|opens| B[LinearIssueBrowser]
    B -->|resolveActions.onOpenModal| C{LinearIssueResolveModalKind}
    C -->|create-lane| D[CreateLaneAttachedModal]
    C -->|resolve-new-lane| E[ResolveInNewLaneModal]
    C -->|resolve-existing-lane| F[ResolveInExistingLaneModal]
    D -->|onConfirm| G[createLaneForIssue]
    E -->|onConfirm modelId| G
    G -->|lane created| H[laneService.create auto-allocates color]
    E -->|after lane created| I[openWorkDraftForLinearIssue]
    F -->|onConfirm laneId+modelId| I
    I -->|requestLinearIssueWorkContext TTL=60s| J[linearIssueWorkNavigation singleton]
    I -->|navigate| K[Work #/work?laneId=...]
    K -->|peekLinearIssueWorkContext| L[AgentChatPane initialLinearIssueContext]
    L -->|consumed| M[consumeLinearIssueWorkContext clears singleton]
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx, line 1328-1337 (link)

    P2 busyModal flicker in handleResolveInExistingLane

    handleResolveInExistingLane is declared async and wraps a synchronous block in try/finally. setBusyModal("resolve-existing-lane") is set and then immediately cleared in the same microtask, so the loading indicator on the resolve-action buttons never renders. Consider either making the function synchronous or adding an await before openWorkDraft if async navigation is expected.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
    Line: 1328-1337
    
    Comment:
    **`busyModal` flicker in `handleResolveInExistingLane`**
    
    `handleResolveInExistingLane` is declared `async` and wraps a synchronous block in `try/finally`. `setBusyModal("resolve-existing-lane")` is set and then immediately cleared in the same microtask, so the loading indicator on the resolve-action buttons never renders. Consider either making the function synchronous or adding an `await` before `openWorkDraft` if async navigation is expected.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/desktop/src/renderer/components/lanes/linearProjectIcon.tsx:141-148
`asString` returns `""` (not `null`) when a Linear project has no color set, so `"" ?? LINEAR_BRAND.primaryBright` passes the empty string through — `??` only substitutes on `null`/`undefined`. The resulting `background: ""` is invalid CSS, leaving the letter-initial chip with no visible background. The glyph branch above already handles this correctly via `projectIconBackground`, which treats empty strings as falsy. Use `||` here for consistency.

```suggestion
  return (
    <span
      className="grid shrink-0 place-items-center rounded-[4px] text-[9px] font-semibold text-white/90"
      style={{
        width: size,
        height: size,
        background: color || LINEAR_BRAND.primaryBright,
      }}
```

Reviews (3): Last reviewed commit: "ship: iteration 1 — fix test-desktop (2)..." | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview May 26, 2026 11:56pm

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 26, 2026

@copilot review but do not make fixes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

Adds a browser runtime bridge with Vite live preview, implements Linear issue resolve modals and quick view, refactors Work/Terminals chrome and sessions UI, introduces lane color allocation and custom VCS icons, updates chat drawers/composer and various components/tests, and overhauls related CSS.

Changes

End-to-end desktop renderer updates: bridge, Linear flows, Work chrome, lane colors/icons, and UI refactors

Layer / File(s) Summary
Full change set walkthrough
...
Single checkpoint covering the browser runtime bridge/dev scripts, Linear client and resolve UI, Work/Terminals chrome refactor, lane color allocation and VCS icons, chat actions drawer/composer tweaks, files UI updates, tests, and CSS theme/layout changes.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • arul28/ADE#327 — Also changes HeaderUsageControl; API/trigger behavior overlaps.
  • arul28/ADE#361 — Touches tuiClient/app.tsx near optimistic session handling.
  • arul28/ADE#274 — Earlier Linear UI work; this PR extends Linear issue browser/quick view flows.

Suggested labels

desktop

✨ 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/ui-clean-up-3470f34e

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 26, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

PR Review

Scope: 97 file(s), +5381 / −2329
Verdict: Needs changes

This PR refreshes desktop shell styling, consolidates Work/chat chrome, adds Linear issue resolve modals (create lane / new chat flows), a Vite browser-runtime bridge for local preview, and fixes the Linear addedLabelIds mutation. The main gaps are silent failures on lane creation and fragile handoff of Linear issue context into Work.


🔒 Security

[Medium] Dev browser bridge allows cross-origin RPC from any website

File: apps/desktop/scripts/browser-runtime-bridge.mjs:L79-L86 and L216-L227
Issue: The bridge listens on 127.0.0.1 but responds with Access-Control-Allow-Origin: *, and /sync forwards arbitrary method/params to the ADE JSON-RPC socket with no shared secret.
Attack path: While dev:vite:live / dev:browser-bridge is running, a malicious page the user has open in another tab can fetch("http://127.0.0.1:18765/sync", …) (or /action) and invoke project-scoped RPC (sync pairing, lane actions, Linear token setup via the patched mock path).
Fix: Drop Access-Control-Allow-Origin: *; allow only the Vite origin (e.g. http://localhost:5173), or require a bridge token header generated at startup and checked on every POST. Prefer proxying only through Vite without a directly reachable bridge port.


🐛 Functionality

[High] Linear resolve modals hide lane creation failures

File: apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx:L207-L228
Issue: handleCreateLaneAttached and handleResolveInNewLane await createLaneForIssue() inside try/finally with no catch. When lanes.create rejects (e.g. branch already exists for the issue slug), the promise rejects, busy state clears, and the modal stays open with no error surfaced.
Repro: Connect Linear → open quick view → pick an issue that already has a lane/branch → Resolve issue in new chat in new lane (or Create lane attached) again.
Fix: Wrap creation in try/catch, show the error in the modal (or app toast), and only call closeModal() / navigate on success.

[Medium] Same Linear issue is not re-attached on a second Work open

File: apps/desktop/src/renderer/components/chat/AgentChatPane.tsx:L4691-L4704
Issue: consumedInitialLinearIssueContextRef is keyed only by initialLinearIssueContext.id. Resolving the same issue into a different lane in the same session skips re-attachment because the ref still matches, while onInitialLinearIssueContextConsumed may already have cleared module pending state.
Repro: Resolve ADE-123 into lane A (context attaches) → return to Linear quick view → Resolve … in existing lane B for the same issue → Work opens without the Linear context attachment.
Fix: Key consumption by ${laneId}:${issue.id} (reset when laneId changes) or remount AgentChatPane with key={${laneId}:${issue.id}} from WorkStartSurface.

[Medium] Module-level pending Work context is single-slot

File: apps/desktop/src/renderer/lib/linearIssueWorkNavigation.ts:L11-L26
Issue: pending is one global variable; a second requestLinearIssueWorkContext overwrites the first before Work consumes it.
Repro: Trigger resolve-to-Work for lane A, then quickly for lane B before the Work draft mounts; opening lane A loses its pending issue/model.
Fix: Store a Map<laneId, PendingLinearIssueWorkContext> or pass context through router/store state instead of a singleton.


🎨 UI/UX

[Low] Linear top-bar control stays hidden for 8s after project open

File: apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx:L24 and L94-L102
Issue: Initial visibility waits INITIAL_VISIBILITY_CHECK_DELAY_MS (8s) before the first getLinearConnectionStatus call, so connected users see no Linear entry in the header/menu for several seconds on cold load (focus refresh helps only after interaction).
Fix: Run the first visibility check immediately (keep the delayed path only for disconnected projects to avoid polling), or show a disabled control until the check completes.

[Low] Resolve-in-new-lane confirm gives no feedback when model is empty

File: apps/desktop/src/renderer/components/app/LinearIssueResolveModals.tsx:L191-L196
Issue: Confirm calls if (!modelId.trim()) return; with no inline error; the primary button stays enabled when model recents are empty.
Repro: Open resolve modal with empty model recents → click Create lane and open chat → nothing happens.
Fix: Disable the confirm button when !modelId.trim() and show a short validation hint under the model picker.


Notes

  • The Linear GraphQL fix (addedLabelIds vs labelIds) and lane auto-color allocation look correct.
  • GlowMenu includes aria-label / aria-pressed on tab buttons; good pattern for the new Work tab strip.
  • Browser bridge risk is limited to local dev when the bridge process is running; production Electron IPC is unchanged.
  • Did not run desktop typecheck/tests in this review pass.
Open in Web View Automation 

Sent by Cursor Automation: BUGBOT in Versic

Comment thread apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
Copy link
Copy Markdown
Contributor

Copilot AI commented May 26, 2026

@copilot review but do not make fixes

Found one substantive regression in the reviewed commits: apps/desktop/src/renderer/components/terminals/useWorkSessions.ts still parses/persists statusFilter (including ?status= URL handling), but the filtered list no longer applies that filter, so ended sessions appear even when status is running (see current expectation in useWorkSessions.test.ts around the status-filter test). No fixes made, per request.

Copilot finished work on behalf of arul28 May 26, 2026 23:08
Copy link
Copy Markdown

@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: 12

Caution

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

⚠️ Outside diff range comments (4)
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx (3)

6985-7047: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hide or disable the Chat actions trigger until a session exists.

When selectedSessionId is null, this button is still rendered, but the empty-state branch never renders chatActionsPanelContent. Clicking it toggles the state without ever showing a drawer.

Suggested fix
-          {(showWorkspaceChrome && laneId) || canShowHandoff ? (
+          {selectedSessionId && ((showWorkspaceChrome && laneId) || canShowHandoff) ? (
             <SmartTooltip
               content={{
                 label: chatActionsOpen ? "Close chat actions" : "Open chat actions",
🤖 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/chat/AgentChatPane.tsx` around lines
6985 - 7047, The Chat actions button can be toggled even when no session exists
(selectedSessionId is null) which opens nothing because chatActionsPanelContent
is empty; update the render and click logic in AgentChatPane so the button is
hidden or disabled when selectedSessionId is falsy: include selectedSessionId in
the conditional that renders the SmartTooltip/button (or add an early guard)
and, inside the onClick handler for setChatActionsOpen, no-op if
selectedSessionId is null (prevent toggling); adjust aria-disabled/tooltip text
accordingly and reference chatActionsOpen, setChatActionsOpen,
selectedSessionId, and chatActionsPanelContent when making the change.

6953-6961: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close Cursor Cloud when opening App Control from the header.

This toggle leaves cursorCloudPaneOpen untouched, so it's possible to end up with both right-side panes mounted at once. The composer-level App Control toggle already closes Cursor Cloud, so this path should match it.

Suggested fix
                 onClick={() => {
                   setAppControlOpen((current) => {
                     const next = !current;
                     if (next) {
                       setChatActionsOpen(false);
                       setIosSimulatorOpen(false);
+                      setCursorCloudPaneOpen(false);
                     }
                     return next;
                   });
                 }}
🤖 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/chat/AgentChatPane.tsx` around lines
6953 - 6961, When toggling App Control via the header in the onClick handler for
setAppControlOpen, ensure Cursor Cloud is closed the same way the composer-level
toggle does: when computing next = !current, if next is true call
setChatActionsOpen(false), setIosSimulatorOpen(false) and also call
setCursorCloudPaneOpen(false) so the Cursor Cloud pane is unmounted when App
Control opens; leave behavior unchanged when closing (i.e., only close Cursor
Cloud on open).

4125-4135: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't clear the restored chat-actions state here.

This effect runs after the new companion-state hydration and forces chatActionsOpen back to false on every session/lane switch, so the per-session sessionStorage state you just added never actually survives a restore.

Suggested fix
   useEffect(() => {
     setAttachments([]);
     setContextAttachments([]);
     setPromptSuggestion(null);
-    setChatActionsOpen(false);
     setHandoffBusy(false);
     optimisticOutgoingMessageRef.current = null;
     setOptimisticOutgoingMessage(null);
🤖 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/chat/AgentChatPane.tsx` around lines
4125 - 4135, The effect that resets state on session/lane change currently
forces chatActionsOpen to false which overrides the per-session restore from
sessionStorage; remove the unconditional setChatActionsOpen(false) (or guard it
by checking whether a restored value exists) inside the useEffect that depends
on selectedSessionId and laneId so the hydrated chat-actions state can survive a
session/lane switch; keep the other resets (setAttachments,
setContextAttachments, setPromptSuggestion, setHandoffBusy,
optimisticOutgoingMessageRef.current, setOptimisticOutgoingMessage) unchanged.
apps/desktop/src/renderer/components/terminals/LaneCombobox.tsx (1)

161-185: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore visible/semantic active option state for keyboard navigation.

highlightedIndex changes on Arrow keys (Line 169 onward), but options don’t render any highlighted/active state anymore (Line 425 onward). Keyboard users can’t tell which option Enter will select.

Suggested patch
-              <div ref={listRef} className="ade-lane-popover-list">
+              <div ref={listRef} className="ade-lane-popover-list" role="listbox">
                 {items.length === 0 ? (
@@
-                  items.map((item) => {
+                  items.map((item, index) => {
                     const isSelected = item.id === value;
                     const isAutoCreate = item.id === AUTO_CREATE_LANE_OPTION_ID;
@@
                         <button
                           key={item.id}
                           type="button"
+                          role="option"
+                          aria-selected={isSelected}
                           className="ade-lane-popover-item ade-lane-popover-item-featured"
                           data-selected={isSelected ? "true" : undefined}
+                          data-highlighted={index === highlightedIndex ? "true" : undefined}
+                          onMouseEnter={() => setHighlightedIndex(index)}
                           onClick={() => selectItem(item.id)}
                         >
@@
                       <button
                         key={item.id}
                         type="button"
+                        role="option"
+                        aria-selected={isSelected}
                         className="ade-lane-popover-item"
                         data-selected={isSelected ? "true" : undefined}
+                        data-highlighted={index === highlightedIndex ? "true" : undefined}
+                        onMouseEnter={() => setHighlightedIndex(index)}
                         onClick={() => selectItem(item.id)}

As per coding guidelines, apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices.

Also applies to: 412-517

🤖 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/terminals/LaneCombobox.tsx` around lines
161 - 185, The keyboard handler (handleKeyDown) updates highlightedIndex but the
option render path no longer reflects that state, so restore visible and
semantic active state by: in the options rendering code (where items are mapped
and option elements are created) add a comparison (index === highlightedIndex)
to apply a visible active CSS class and set aria-selected or aria-current
accordingly, give each option a stable id and set the container's
aria-activedescendant to that id when highlighted, and ensure the Enter handler
(selectItem) continues to use items[highlightedIndex] so keyboard selection
matches the visible/semantic highlight; update any role attributes to
role="listbox"/role="option" if missing to support screen readers.
🧹 Nitpick comments (2)
apps/desktop/src/renderer/components/app/TopBar.tsx (1)

329-330: ⚡ Quick win

Avoid partial ARIA menu semantics in the compact status popover.

The popover is marked as a menu/menuitem, but it behaves like regular tabbable buttons (no menu keyboard model like roving focus/arrow key navigation). This can confuse assistive tech. Either implement full menu semantics or switch to dialog/popover semantics with native button roles.

As per coding guidelines apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices.

Also applies to: 427-429, 447-449

🤖 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/app/TopBar.tsx` around lines 329 - 330,
The TopBar component currently assigns menu/menuitem roles to the compact status
popover (e.g., the element using role={layout === "menu-row" ? "menuitem" :
undefined} and related elements), but it does not implement the full ARIA menu
keyboard model; update the component to either implement full menu semantics
(roving focus, arrow-key navigation, proper aria-activedescendant management)
for the elements that use role="menu"/"menuitem" or, preferably, remove the
partial menu semantics and switch to proper popover/dialog semantics by removing
role="menu"/"menuitem", using native <button> elements or role="button", and
adding aria-haspopup, aria-expanded, and aria-controls as appropriate for the
popover trigger; make the same change for the other instances in this file where
menu/menuitem roles are set (the other compact status popover occurrences).
apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx (1)

1893-1904: ⚡ Quick win

Extract duplicated “New Chat” tab-strip button into a shared renderer.

The same JSX appears in both grouped and ungrouped tab-strip paths; extracting it will prevent style/behavior drift.

Suggested refactor
+ function NewChatTabButton({ onClick }: { onClick: () => void }) {
+   return (
+     <SmartTooltip content={{ label: "New Chat", description: "Start a new AI chat session in the current lane." }}>
+       <button
+         type="button"
+         className="ade-work-new-chat-btn inline-flex shrink-0 items-center justify-center"
+         onClick={onClick}
+         aria-label="Start a new chat"
+       >
+         <Plus size={11} weight="bold" />
+       </button>
+     </SmartTooltip>
+   );
+ }
...
- {visibleSessions.length === 0 ? (
-   <SmartTooltip content={{ label: "New Chat", description: "Start a new AI chat session in the current lane." }}>
-     <button ...>
-       <Plus size={11} weight="bold" />
-     </button>
-   </SmartTooltip>
- ) : null}
+ {visibleSessions.length === 0 ? <NewChatTabButton onClick={() => onShowDraftKind("chat")} /> : null}

Also applies to: 2057-2068

🤖 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/terminals/WorkViewArea.tsx` around lines
1893 - 1904, The duplicated "New Chat" tab-strip button JSX in WorkViewArea.tsx
(appearing in both grouped and ungrouped tab-strip paths) should be extracted
into a single renderer to avoid drift: create a small local component or render
function (e.g., NewChatButton or renderNewChatButton) that accepts the needed
props (at minimum onShowDraftKind) and returns the SmartTooltip-wrapped button
with the same className, onClick handler, aria-label and Plus icon props; then
replace the duplicated blocks in both tab-strip branches with this new renderer
so styling and behavior remain consistent.
🤖 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/scripts/browser-runtime-bridge.mjs`:
- Around line 79-85: Replace the wildcard CORS header by validating the incoming
Origin and returning a specific allowed origin: update sendJson to read the
request Origin, compare it to a small allowlist (configured via env or a
constant) and only set "Access-Control-Allow-Origin" to the matched origin (omit
or return 403 if not allowed); apply the same Origin-allowlist logic to the
other runtime endpoints/handlers in the file (the handlers in the region
covering the runtime action/sync endpoints referenced around lines 171-245) and
ensure preflight (OPTIONS) responses use the same allowlist and allowed
methods/headers.

In `@apps/desktop/scripts/dev-vite-live.mjs`:
- Around line 72-83: The bridge exit handler currently doesn't propagate its
exit/signal to the wrapper; capture the bridge exit results by storing the exit
code and signal in module-scope variables inside bridge.on("exit", (code,
signal) => ...) (e.g., bridgeExitCode, bridgeExitSignal) before calling
shutdown("SIGTERM"). Then change the vite.on handler to accept (code, signal)
and when deciding process.exit, prefer a non-zero exit: if code is a number use
it, otherwise if signal is set convert it to an exit code via
util.convertProcessSignalToExitCode(signal) (or use the stored
bridgeExitSignal/bridgeExitCode if the bridge triggered shutdown) and pass that
to process.exit so signal terminations are not treated as success.

In `@apps/desktop/src/renderer/browserRuntimeBridge.ts`:
- Around line 87-116: The patch currently replaces ade.cto.setLinearToken and
ade.cto.clearLinearToken with bridge-backed calls but does not update the
renderer mock API aliases setLinearOAuthClient and clearLinearOAuthClient,
leaving the OAuth set/clear behavior unmapped; inside patchLinearMethods
add/update ade.cto.setLinearOAuthClient and ade.cto.clearLinearOAuthClient to
perform the same bridgePost actions as ade.cto.setLinearToken and
ade.cto.clearLinearToken (use the same domains/actions:
"linear_credentials"/"setToken" and "clearToken" then return the
"linear_issue_tracker"/"getConnectionStatus" bridgePost) so both the old and new
method names are backed by the bridge.

In `@apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx`:
- Around line 164-172: The hasActiveFilters function treats a whitespace-only
search as an active filter because it compares filters.query directly; change
the comparison to use the normalized query (e.g., filters.query.trim()) so that
whitespace-only queries are considered equal to DEFAULT_FILTERS.query. Update
the check in hasActiveFilters to normalize filters.query before comparing
(reference hasActiveFilters, DEFAULT_FILTERS, and the search usage that calls
filters.query.trim()) so Reset filters and persistence behavior match the actual
search logic.

In `@apps/desktop/src/renderer/components/app/LinearIssueResolveModals.tsx`:
- Around line 280-283: The function openLinearIssueExternalUrl currently calls
window.ade.app.openExternal directly which can fail if the IPC bridge is
unavailable; change it to use the shared helper openExternalUrl(url) instead
(preserve the early return when url is null/undefined) so failures are handled
safely and avoid unhandled rejections; update references inside
openLinearIssueExternalUrl to call openExternalUrl with the same url parameter
and remove the direct window.ade.app.openExternal invocation.

In `@apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx`:
- Around line 207-240: The three handlers (handleCreateLaneAttached,
handleResolveInNewLane, handleResolveInExistingLane) currently use try/finally
only, so async rejections (from createLaneForIssue, openWorkDraft, etc.) can
become unhandled; add explicit catch blocks to each async handler to log the
error (use the same logger/context you already use in this file), show a
user-visible error (toast/modal) and ensure closeModal() still runs, e.g., move
closeModal into try or call it in both success and error paths, and keep
setBusyModal(null) in finally; reference createLaneForIssue, openLaneInLanesTab,
openWorkDraft, setBusyModal and closeModal when implementing these changes.

In `@apps/desktop/src/renderer/components/lanes/linearProjectIcon.tsx`:
- Around line 147-148: Validate and normalize the tile background color before
applying it to the style: instead of using the raw color prop in the style
(background: color ?? LINEAR_BRAND.primaryBright), run the color through the
same normalization routine used by the glyph path (the glyph's color
normalization function or helper) and fall back to a normalized
LINEAR_BRAND.primaryBright when the normalized value is invalid; update the
background assignment to use that normalized/fallback value so invalid CSS
values are never applied.

In `@apps/desktop/src/renderer/components/terminals/LaneChip.tsx`:
- Around line 155-156: The onContextMenu handler is being forwarded directly
(onLaneContextMenu) which lets parent/context handlers also run; update the
places where onContextMenu is passed (the LaneChip lane control elements using
onLaneContextMenu) to wrap the callback in a short inline handler that calls
event.preventDefault() and event.stopPropagation() and then invokes
onLaneContextMenu(event) (only if handler exists); apply the same change to both
occurrences so context-menu events are isolated like click/mousedown handlers.

In `@apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx`:
- Around line 1412-1417: The close button (data-close-tab-session-id, className
"ade-work-tab-close", title using isBusy) is only revealed on hover so keyboard
users cannot see it when focused; update the button's styling to also become
visible on keyboard focus by adding focus-visible and/or focus state classes
(e.g., focus:opacity-100 or focus-visible:opacity-100 and an appropriate focus
ring like focus:outline-none focus:ring) so tabbing shows the control and
maintains the existing hover behavior; ensure the change is applied to the same
button element inside WorkViewArea.tsx so keyboard users can access the "Remove
from Work view" action.

In `@apps/desktop/src/renderer/index.css`:
- Line 451: Add a blank line before the CSS declaration "height:
var(--shell-project-tab-height);" (and the other identical declaration flagged
by stylelint) so the rule declaration-empty-line-before is satisfied; if the
declaration is the first property in its ruleset, instead add a stylelint
exemption comment (stylelint-disable-next-line declaration-empty-line-before)
immediately above that declaration to avoid a false positive. Ensure you update
both occurrences flagged by the linter.

In `@apps/desktop/src/renderer/lib/linearIssueWorkNavigation.ts`:
- Around line 29-39: The current peekLinearIssueWorkContext and
consumeLinearIssueWorkContext read pending without validating its requestedAt
timestamp, allowing stale contexts to be returned; update both functions to
accept a requestedAt (or compare to now) and only return/consume pending when
pending.laneId === laneId AND pending.requestedAt matches the provided
requestedAt (or pending.requestedAt is within a MAX_PENDING_AGE_MS threshold);
if the timestamp check fails, return null and do not clear pending (for peek) or
clear only when the timestamp matches (for consume). Ensure you reference and
use the pending.requestedAt property, add a MAX_PENDING_AGE_MS constant if using
age-based validation, and update both function signatures
(peekLinearIssueWorkContext, consumeLinearIssueWorkContext) and their callers
accordingly.

In `@apps/desktop/src/shared/laneColorPalette.ts`:
- Around line 63-67: The function laneColorName does not trim the input hex
string, so whitespace-padded values won't match; update laneColorName to first
trim the incoming hex (e.g., const normalized = hex.trim()), return null if
normalized is empty, then use normalized.toLowerCase() when comparing against
LANE_COLOR_PALETTE entries (entry.hex.toLowerCase()) so trimmed values resolve
correctly; keep using LANE_COLOR_PALETTE and the existing find/?.name logic.

---

Outside diff comments:
In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 6985-7047: The Chat actions button can be toggled even when no
session exists (selectedSessionId is null) which opens nothing because
chatActionsPanelContent is empty; update the render and click logic in
AgentChatPane so the button is hidden or disabled when selectedSessionId is
falsy: include selectedSessionId in the conditional that renders the
SmartTooltip/button (or add an early guard) and, inside the onClick handler for
setChatActionsOpen, no-op if selectedSessionId is null (prevent toggling);
adjust aria-disabled/tooltip text accordingly and reference chatActionsOpen,
setChatActionsOpen, selectedSessionId, and chatActionsPanelContent when making
the change.
- Around line 6953-6961: When toggling App Control via the header in the onClick
handler for setAppControlOpen, ensure Cursor Cloud is closed the same way the
composer-level toggle does: when computing next = !current, if next is true call
setChatActionsOpen(false), setIosSimulatorOpen(false) and also call
setCursorCloudPaneOpen(false) so the Cursor Cloud pane is unmounted when App
Control opens; leave behavior unchanged when closing (i.e., only close Cursor
Cloud on open).
- Around line 4125-4135: The effect that resets state on session/lane change
currently forces chatActionsOpen to false which overrides the per-session
restore from sessionStorage; remove the unconditional setChatActionsOpen(false)
(or guard it by checking whether a restored value exists) inside the useEffect
that depends on selectedSessionId and laneId so the hydrated chat-actions state
can survive a session/lane switch; keep the other resets (setAttachments,
setContextAttachments, setPromptSuggestion, setHandoffBusy,
optimisticOutgoingMessageRef.current, setOptimisticOutgoingMessage) unchanged.

In `@apps/desktop/src/renderer/components/terminals/LaneCombobox.tsx`:
- Around line 161-185: The keyboard handler (handleKeyDown) updates
highlightedIndex but the option render path no longer reflects that state, so
restore visible and semantic active state by: in the options rendering code
(where items are mapped and option elements are created) add a comparison (index
=== highlightedIndex) to apply a visible active CSS class and set aria-selected
or aria-current accordingly, give each option a stable id and set the
container's aria-activedescendant to that id when highlighted, and ensure the
Enter handler (selectItem) continues to use items[highlightedIndex] so keyboard
selection matches the visible/semantic highlight; update any role attributes to
role="listbox"/role="option" if missing to support screen readers.

---

Nitpick comments:
In `@apps/desktop/src/renderer/components/app/TopBar.tsx`:
- Around line 329-330: The TopBar component currently assigns menu/menuitem
roles to the compact status popover (e.g., the element using role={layout ===
"menu-row" ? "menuitem" : undefined} and related elements), but it does not
implement the full ARIA menu keyboard model; update the component to either
implement full menu semantics (roving focus, arrow-key navigation, proper
aria-activedescendant management) for the elements that use
role="menu"/"menuitem" or, preferably, remove the partial menu semantics and
switch to proper popover/dialog semantics by removing role="menu"/"menuitem",
using native <button> elements or role="button", and adding aria-haspopup,
aria-expanded, and aria-controls as appropriate for the popover trigger; make
the same change for the other instances in this file where menu/menuitem roles
are set (the other compact status popover occurrences).

In `@apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx`:
- Around line 1893-1904: The duplicated "New Chat" tab-strip button JSX in
WorkViewArea.tsx (appearing in both grouped and ungrouped tab-strip paths)
should be extracted into a single renderer to avoid drift: create a small local
component or render function (e.g., NewChatButton or renderNewChatButton) that
accepts the needed props (at minimum onShowDraftKind) and returns the
SmartTooltip-wrapped button with the same className, onClick handler, aria-label
and Plus icon props; then replace the duplicated blocks in both tab-strip
branches with this new renderer so styling and behavior remain consistent.
🪄 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: efe3eb7f-66b9-4fa5-9cb5-639bd728c70d

📥 Commits

Reviewing files that changed from the base of the PR and between 5e38243 and b904257.

⛔ Files ignored due to path filters (7)
  • README.md is excluded by !*.md
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/agents/README.md is excluded by !docs/**
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/history/README.md is excluded by !docs/**
  • docs/features/lanes/README.md is excluded by !docs/**
  • docs/features/linear-integration/README.md is excluded by !docs/**
📒 Files selected for processing (90)
  • apps/ade-cli/src/tuiClient/app.tsx
  • apps/desktop/README.md
  • apps/desktop/package.json
  • apps/desktop/resources/agent-skills/ade-app-control/SKILL.md
  • apps/desktop/scripts/browser-runtime-bridge.mjs
  • apps/desktop/scripts/dev-vite-live.mjs
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/ai/tools/orchestrationTools.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearClient.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/notifications/apnsBridgeService.ts
  • apps/desktop/src/main/services/orchestration/manifestNormalization.ts
  • apps/desktop/src/main/services/orchestration/orchestrationService.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/browserRuntimeBridge.ts
  • apps/desktop/src/renderer/components/app/CommandPalette.tsx
  • apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
  • apps/desktop/src/renderer/components/app/LinearIssueResolveModals.tsx
  • apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
  • apps/desktop/src/renderer/components/app/TabNav.test.tsx
  • apps/desktop/src/renderer/components/app/TabNav.tsx
  • apps/desktop/src/renderer/components/app/TopBar.test.tsx
  • apps/desktop/src/renderer/components/app/TopBar.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatActionsDrawerPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatComposerShell.tsx
  • apps/desktop/src/renderer/components/chat/ChatGitToolbar.test.tsx
  • apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatSurfaceShell.tsx
  • apps/desktop/src/renderer/components/chat/ChatTerminalDrawer.tsx
  • apps/desktop/src/renderer/components/files/FilesExplorer.tsx
  • apps/desktop/src/renderer/components/files/FilesPage.test.tsx
  • apps/desktop/src/renderer/components/files/FilesPage.tsx
  • apps/desktop/src/renderer/components/history/CommitHistoryView.tsx
  • apps/desktop/src/renderer/components/history/HistoryPage.tsx
  • apps/desktop/src/renderer/components/history/historyGitActions.ts
  • apps/desktop/src/renderer/components/history/historyLaneActions.ts
  • apps/desktop/src/renderer/components/lanes/BranchPickerView.tsx
  • apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx
  • apps/desktop/src/renderer/components/lanes/LanesPage.tsx
  • apps/desktop/src/renderer/components/lanes/ManageLaneDialog.tsx
  • apps/desktop/src/renderer/components/lanes/laneColorPalette.ts
  • apps/desktop/src/renderer/components/lanes/linearProjectIcon.tsx
  • apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx
  • apps/desktop/src/renderer/components/orchestration/PlanMarkdown.tsx
  • apps/desktop/src/renderer/components/orchestration/TaskCard.tsx
  • apps/desktop/src/renderer/components/prs/CreatePrModal.tsx
  • apps/desktop/src/renderer/components/prs/detail/PrDetailPane.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrLaneCleanupBanner.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrRequestAiReviewDialog.tsx
  • apps/desktop/src/renderer/components/review/ReviewPage.tsx
  • apps/desktop/src/renderer/components/run/RunPage.test.tsx
  • apps/desktop/src/renderer/components/settings/ChatAppearancePreview.tsx
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.tsx
  • apps/desktop/src/renderer/components/terminals/LaneChip.test.tsx
  • apps/desktop/src/renderer/components/terminals/LaneChip.tsx
  • apps/desktop/src/renderer/components/terminals/LaneCombobox.tsx
  • apps/desktop/src/renderer/components/terminals/OrchestratorComposerEntry.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx
  • apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx
  • apps/desktop/src/renderer/components/terminals/WorkStartSurface.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
  • apps/desktop/src/renderer/components/ui/FloatingPane.tsx
  • apps/desktop/src/renderer/components/ui/GlowMenu.tsx
  • apps/desktop/src/renderer/components/ui/LaneBranchInline.tsx
  • apps/desktop/src/renderer/components/ui/PaneTilingLayout.tsx
  • apps/desktop/src/renderer/components/ui/vcsIcons.tsx
  • apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/renderer/lib/laneNavigation.ts
  • apps/desktop/src/renderer/lib/linearIssueWorkNavigation.ts
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/laneColorPalette.ts
  • apps/desktop/src/shared/types/cto.ts
  • apps/desktop/vite.config.ts
  • scripts/dev-shared.mjs
💤 Files with no reviewable changes (5)
  • apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx
  • apps/desktop/src/main/services/orchestration/manifestNormalization.ts
  • apps/desktop/src/renderer/components/chat/ChatGitToolbar.test.tsx
  • apps/desktop/src/main/services/ai/tools/orchestrationTools.ts
  • apps/desktop/src/renderer/components/orchestration/TaskCard.tsx

Comment on lines +79 to +85
function sendJson(res, statusCode, payload) {
res.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict bridge origins for privileged runtime actions.

The bridge currently allows Access-Control-Allow-Origin: * while exposing generic runtime action/sync endpoints. Any web page can attempt cross-origin calls to this localhost service during dev:vite:live.

🔒 Suggested hardening
+const allowedOrigins = new Set([
+  "http://localhost:5173",
+  "http://127.0.0.1:5173",
+]);
+
+function resolveAllowedOrigin(req) {
+  const origin = req.headers.origin;
+  return typeof origin === "string" && allowedOrigins.has(origin) ? origin : null;
+}
+
-function sendJson(res, statusCode, payload) {
+function sendJson(res, statusCode, payload, origin = null) {
   res.writeHead(statusCode, {
     "Content-Type": "application/json; charset=utf-8",
-    "Access-Control-Allow-Origin": "*",
+    ...(origin ? { "Access-Control-Allow-Origin": origin, Vary: "Origin" } : {}),
     "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
     "Access-Control-Allow-Headers": "Content-Type",
   });
   res.end(JSON.stringify(payload));
 }
 
 const server = http.createServer(async (req, res) => {
+  const origin = resolveAllowedOrigin(req);
+
   if (req.method === "OPTIONS") {
-    sendJson(res, 204, {});
+    if (!origin) {
+      sendJson(res, 403, { ok: false, error: "Forbidden origin." });
+      return;
+    }
+    sendJson(res, 204, {}, origin);
     return;
   }
+
+  if (req.method === "POST" && !origin) {
+    sendJson(res, 403, { ok: false, error: "Forbidden origin." });
+    return;
+  }

Also applies to: 171-245

🤖 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/scripts/browser-runtime-bridge.mjs` around lines 79 - 85,
Replace the wildcard CORS header by validating the incoming Origin and returning
a specific allowed origin: update sendJson to read the request Origin, compare
it to a small allowlist (configured via env or a constant) and only set
"Access-Control-Allow-Origin" to the matched origin (omit or return 403 if not
allowed); apply the same Origin-allowlist logic to the other runtime
endpoints/handlers in the file (the handlers in the region covering the runtime
action/sync endpoints referenced around lines 171-245) and ensure preflight
(OPTIONS) responses use the same allowlist and allowed methods/headers.

Comment on lines +72 to +83
bridge.on("exit", (code, signal) => {
if (shuttingDown) return;
process.stderr.write(
`[ade] browser runtime bridge exited (${signal ?? code ?? "unknown"})\n`,
);
shutdown("SIGTERM");
});

vite.on("exit", (code) => {
shutdown("SIGTERM");
process.exit(typeof code === "number" ? code : 0);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the current exit-path behavior in the wrapper.
rg -n -C3 'bridge\.on\("exit"|vite\.on\("exit"|process\.exit' apps/desktop/scripts/dev-vite-live.mjs

Repository: arul28/ADE

Length of output: 698


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/desktop/scripts/dev-vite-live.mjs"

# Show the shutdown helper + surrounding exit handlers
nl -ba "$FILE" | sed -n '1,140p'

# Specifically locate any `shutdown(` implementation and any `bridge.kill/vite.kill` usage
rg -n "function shutdown|const shutdown|shutdown\(" "$FILE"
rg -n "bridge\.kill|vite\.kill|shuttingDown|bridge\.on\(\"exit\"|vite\.on\(\"exit\"" "$FILE"

Repository: arul28/ADE

Length of output: 97


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/desktop/scripts/dev-vite-live.mjs"

# Show the relevant region without `nl`
cat -n "$FILE" | sed -n '1,160p'

rg -n "function shutdown|const shutdown|shutdown\(" "$FILE" || true
rg -n "bridge\.kill|vite\.kill|shuttingDown|bridge\.on\\(\"exit\"\\)|vite\.on\\(\"exit\"\\)|process\.exit" "$FILE" || true

Repository: arul28/ADE

Length of output: 3630


🌐 Web query:

Node.js child_process 'exit' event (code, signal) code null when killed by signal documentation

💡 Result:

In Node.js child process handling, the 'exit' event callback parameters are (code, signal). When a child process is killed by a signal, code is null and signal is the signal name (e.g., 'SIGKILL'), and Node guarantees exactly one of code or signal is non-null. [1] Specifically, Node documents that for the child process 'exit' event: code is the final exit code if the child exited on its own, or null if it terminated due to a signal; signal is the string name of the signal if it was terminated due to a signal, or null otherwise. [1] Node also notes that when code is null due to signal termination, you can convert the signal to a POSIX exit code using util.convertProcessSignalToExitCode. [1] Example shape (matches the documentation behavior): child.on('exit', (code, signal) => { /* code === null when killed by signal */ }); [1] References: Node.js official child_process documentation. [1]

Citations:


Propagate bridge termination as a non-zero wrapper exit code (don’t map signal exits to 0)

In apps/desktop/scripts/dev-vite-live.mjs (lines 72-83), the wrapper exits 0 when the vite child terminates due to a signal: vite.on("exit", (code) => process.exit(typeof code === "number" ? code : 0)). Node’s child_process exit event provides code === null when killed by a signal, and the current handler treats that as success. Since a bridge exit triggers shutdown("SIGTERM"), bridge failures can be masked as a successful wrapper exit.

  • Store the bridge’s exit code/signal in bridge.on("exit", ...).
  • Update vite.on("exit", ...) to accept (code, signal) and exit non-zero when code is null/signal is set (prefer util.convertProcessSignalToExitCode(signal)).
🤖 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/scripts/dev-vite-live.mjs` around lines 72 - 83, The bridge exit
handler currently doesn't propagate its exit/signal to the wrapper; capture the
bridge exit results by storing the exit code and signal in module-scope
variables inside bridge.on("exit", (code, signal) => ...) (e.g., bridgeExitCode,
bridgeExitSignal) before calling shutdown("SIGTERM"). Then change the vite.on
handler to accept (code, signal) and when deciding process.exit, prefer a
non-zero exit: if code is a number use it, otherwise if signal is set convert it
to an exit code via util.convertProcessSignalToExitCode(signal) (or use the
stored bridgeExitSignal/bridgeExitCode if the bridge triggered shutdown) and
pass that to process.exit so signal terminations are not treated as success.

Comment on lines +87 to +116
function patchLinearMethods(ade: any) {
ade.cto.getLinearConnectionStatus = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getConnectionStatus" });
ade.cto.getLinearQuickView = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getQuickView" });
ade.cto.getLinearIssuePickerData = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getIssuePickerData" });
ade.cto.searchLinearIssues = async (args: Record<string, unknown> = {}) =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "searchIssues", args });
ade.cto.getLinearProjects = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "listProjects" });
ade.cto.setLinearToken = async (args: { token: string }) => {
await bridgePost("/action", {
domain: "linear_credentials",
action: "setToken",
arg: args.token,
});
return bridgePost("/action", {
domain: "linear_issue_tracker",
action: "getConnectionStatus",
});
};
ade.cto.clearLinearToken = async () => {
await bridgePost("/action", { domain: "linear_credentials", action: "clearToken" });
return bridgePost("/action", {
domain: "linear_issue_tracker",
action: "getConnectionStatus",
});
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Patch the existing Linear OAuth method names, not only new aliases.

Line 98 and Line 109 override setLinearToken/clearLinearToken, but the renderer mock API shape in this PR uses setLinearOAuthClient/clearLinearOAuthClient. That leaves OAuth set/clear on mock handlers instead of bridge-backed calls.

Suggested fix
 function patchLinearMethods(ade: any) {
@@
-  ade.cto.setLinearToken = async (args: { token: string }) => {
+  const setLinearOAuthClient = async (args: { token: string }) => {
     await bridgePost("/action", {
       domain: "linear_credentials",
       action: "setToken",
       arg: args.token,
     });
     return bridgePost("/action", {
       domain: "linear_issue_tracker",
       action: "getConnectionStatus",
     });
   };
-  ade.cto.clearLinearToken = async () => {
+  const clearLinearOAuthClient = async () => {
     await bridgePost("/action", { domain: "linear_credentials", action: "clearToken" });
     return bridgePost("/action", {
       domain: "linear_issue_tracker",
       action: "getConnectionStatus",
     });
   };
+  ade.cto.setLinearOAuthClient = setLinearOAuthClient;
+  ade.cto.clearLinearOAuthClient = clearLinearOAuthClient;
+  // Keep backward-compat aliases if any caller still uses old names.
+  ade.cto.setLinearToken = setLinearOAuthClient;
+  ade.cto.clearLinearToken = clearLinearOAuthClient;
 }
📝 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
function patchLinearMethods(ade: any) {
ade.cto.getLinearConnectionStatus = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getConnectionStatus" });
ade.cto.getLinearQuickView = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getQuickView" });
ade.cto.getLinearIssuePickerData = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getIssuePickerData" });
ade.cto.searchLinearIssues = async (args: Record<string, unknown> = {}) =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "searchIssues", args });
ade.cto.getLinearProjects = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "listProjects" });
ade.cto.setLinearToken = async (args: { token: string }) => {
await bridgePost("/action", {
domain: "linear_credentials",
action: "setToken",
arg: args.token,
});
return bridgePost("/action", {
domain: "linear_issue_tracker",
action: "getConnectionStatus",
});
};
ade.cto.clearLinearToken = async () => {
await bridgePost("/action", { domain: "linear_credentials", action: "clearToken" });
return bridgePost("/action", {
domain: "linear_issue_tracker",
action: "getConnectionStatus",
});
};
}
function patchLinearMethods(ade: any) {
ade.cto.getLinearConnectionStatus = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getConnectionStatus" });
ade.cto.getLinearQuickView = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getQuickView" });
ade.cto.getLinearIssuePickerData = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "getIssuePickerData" });
ade.cto.searchLinearIssues = async (args: Record<string, unknown> = {}) =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "searchIssues", args });
ade.cto.getLinearProjects = async () =>
bridgePost("/action", { domain: "linear_issue_tracker", action: "listProjects" });
const setLinearOAuthClient = async (args: { token: string }) => {
await bridgePost("/action", {
domain: "linear_credentials",
action: "setToken",
arg: args.token,
});
return bridgePost("/action", {
domain: "linear_issue_tracker",
action: "getConnectionStatus",
});
};
const clearLinearOAuthClient = async () => {
await bridgePost("/action", { domain: "linear_credentials", action: "clearToken" });
return bridgePost("/action", {
domain: "linear_issue_tracker",
action: "getConnectionStatus",
});
};
ade.cto.setLinearOAuthClient = setLinearOAuthClient;
ade.cto.clearLinearOAuthClient = clearLinearOAuthClient;
// Keep backward-compat aliases if any caller still uses old names.
ade.cto.setLinearToken = setLinearOAuthClient;
ade.cto.clearLinearToken = clearLinearOAuthClient;
}
🤖 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/browserRuntimeBridge.ts` around lines 87 - 116, The
patch currently replaces ade.cto.setLinearToken and ade.cto.clearLinearToken
with bridge-backed calls but does not update the renderer mock API aliases
setLinearOAuthClient and clearLinearOAuthClient, leaving the OAuth set/clear
behavior unmapped; inside patchLinearMethods add/update
ade.cto.setLinearOAuthClient and ade.cto.clearLinearOAuthClient to perform the
same bridgePost actions as ade.cto.setLinearToken and ade.cto.clearLinearToken
(use the same domains/actions: "linear_credentials"/"setToken" and "clearToken"
then return the "linear_issue_tracker"/"getConnectionStatus" bridgePost) so both
the old and new method names are backed by the bridge.

Comment on lines +164 to +172
function hasActiveFilters(filters: LinearIssueBrowserFilters): boolean {
return (
filters.projectId !== DEFAULT_FILTERS.projectId
|| filters.statePreset !== DEFAULT_FILTERS.statePreset
|| filters.assigneeId !== DEFAULT_FILTERS.assigneeId
|| filters.priority !== DEFAULT_FILTERS.priority
|| filters.query !== DEFAULT_FILTERS.query
|| filters.sort !== DEFAULT_FILTERS.sort
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize whitespace query before considering filters active.

On Line 170, filters.query is compared as-is, but search uses filters.query.trim() (Line 360). A whitespace-only query is treated as an active filter, causing no-op state persistence and a misleading “Reset filters” affordance.

Proposed fix
 function hasActiveFilters(filters: LinearIssueBrowserFilters): boolean {
+  const normalizedQuery = filters.query.trim();
   return (
     filters.projectId !== DEFAULT_FILTERS.projectId
     || filters.statePreset !== DEFAULT_FILTERS.statePreset
     || filters.assigneeId !== DEFAULT_FILTERS.assigneeId
     || filters.priority !== DEFAULT_FILTERS.priority
-    || filters.query !== DEFAULT_FILTERS.query
+    || normalizedQuery !== DEFAULT_FILTERS.query
     || filters.sort !== DEFAULT_FILTERS.sort
   );
 }
📝 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
function hasActiveFilters(filters: LinearIssueBrowserFilters): boolean {
return (
filters.projectId !== DEFAULT_FILTERS.projectId
|| filters.statePreset !== DEFAULT_FILTERS.statePreset
|| filters.assigneeId !== DEFAULT_FILTERS.assigneeId
|| filters.priority !== DEFAULT_FILTERS.priority
|| filters.query !== DEFAULT_FILTERS.query
|| filters.sort !== DEFAULT_FILTERS.sort
);
function hasActiveFilters(filters: LinearIssueBrowserFilters): boolean {
const normalizedQuery = filters.query.trim();
return (
filters.projectId !== DEFAULT_FILTERS.projectId
|| filters.statePreset !== DEFAULT_FILTERS.statePreset
|| filters.assigneeId !== DEFAULT_FILTERS.assigneeId
|| filters.priority !== DEFAULT_FILTERS.priority
|| normalizedQuery !== DEFAULT_FILTERS.query
|| filters.sort !== DEFAULT_FILTERS.sort
);
}
🤖 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/app/LinearIssueBrowser.tsx` around lines
164 - 172, The hasActiveFilters function treats a whitespace-only search as an
active filter because it compares filters.query directly; change the comparison
to use the normalized query (e.g., filters.query.trim()) so that whitespace-only
queries are considered equal to DEFAULT_FILTERS.query. Update the check in
hasActiveFilters to normalize filters.query before comparing (reference
hasActiveFilters, DEFAULT_FILTERS, and the search usage that calls
filters.query.trim()) so Reset filters and persistence behavior match the actual
search logic.

Comment on lines +280 to +283
export function openLinearIssueExternalUrl(url: string | null | undefined): void {
if (!url) return;
void window.ade.app.openExternal(url);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the shared external-open helper to avoid bridge failures in renderer.

Line 282 directly calls window.ade.app.openExternal(url). If the bridge is unavailable/rejecting, this can throw or produce unhandled rejections. Use the existing openExternalUrl helper for safe fallback behavior.

Proposed fix
+import { openExternalUrl } from "../../lib/openExternal";
...
 export function openLinearIssueExternalUrl(url: string | null | undefined): void {
-  if (!url) return;
-  void window.ade.app.openExternal(url);
+  openExternalUrl(url);
 }

As per coding guidelines apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices.

📝 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 function openLinearIssueExternalUrl(url: string | null | undefined): void {
if (!url) return;
void window.ade.app.openExternal(url);
}
import { openExternalUrl } from "../../lib/openExternal";
export function openLinearIssueExternalUrl(url: string | null | undefined): void {
openExternalUrl(url);
}
🤖 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/app/LinearIssueResolveModals.tsx` around
lines 280 - 283, The function openLinearIssueExternalUrl currently calls
window.ade.app.openExternal directly which can fail if the IPC bridge is
unavailable; change it to use the shared helper openExternalUrl(url) instead
(preserve the early return when url is null/undefined) so failures are handled
safely and avoid unhandled rejections; update references inside
openLinearIssueExternalUrl to call openExternalUrl with the same url parameter
and remove the direct window.ade.app.openExternal invocation.

Comment on lines +155 to +156
onContextMenu={onLaneContextMenu}
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stop context-menu bubbling on lane controls.

Line 155 and Line 165 forward onContextMenu without stopping propagation, so parent row/menu handlers can fire alongside lane-level handling. Align this with the existing click/mousedown isolation.

Suggested fix
-          onContextMenu={onLaneContextMenu}
+          onContextMenu={(event) => {
+            event.stopPropagation();
+            onLaneContextMenu?.(event);
+          }}
...
-          onContextMenu={onLaneContextMenu}
+          onContextMenu={(event) => {
+            event.stopPropagation();
+            onLaneContextMenu?.(event);
+          }}

As per coding guidelines, apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices.

Also applies to: 165-166

🤖 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/terminals/LaneChip.tsx` around lines 155
- 156, The onContextMenu handler is being forwarded directly (onLaneContextMenu)
which lets parent/context handlers also run; update the places where
onContextMenu is passed (the LaneChip lane control elements using
onLaneContextMenu) to wrap the callback in a short inline handler that calls
event.preventDefault() and event.stopPropagation() and then invokes
onLaneContextMenu(event) (only if handler exists); apply the same change to both
occurrences so context-menu events are isolated like click/mousedown handlers.

Comment on lines 1412 to 1417
<button
type="button"
data-close-tab-session-id={session.id}
title={isBusy ? "Removing..." : "Remove from Work view"}
className="inline-flex items-center justify-center opacity-0 group-hover/tab:opacity-100 transition-opacity"
className="ade-work-tab-close inline-flex items-center justify-center opacity-0 transition-opacity group-hover/tab:opacity-100"
style={{
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make tab-close control visible for keyboard focus.

The close button is hover-revealed only, so keyboard users can tab to an effectively invisible control.

Suggested fix
- className="ade-work-tab-close inline-flex items-center justify-center opacity-0 transition-opacity group-hover/tab:opacity-100"
+ className="ade-work-tab-close inline-flex items-center justify-center opacity-0 transition-opacity group-hover/tab:opacity-100 group-focus-within/tab:opacity-100 focus-visible:opacity-100"

As per coding guidelines, apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices.

📝 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
<button
type="button"
data-close-tab-session-id={session.id}
title={isBusy ? "Removing..." : "Remove from Work view"}
className="inline-flex items-center justify-center opacity-0 group-hover/tab:opacity-100 transition-opacity"
className="ade-work-tab-close inline-flex items-center justify-center opacity-0 transition-opacity group-hover/tab:opacity-100"
style={{
<button
type="button"
data-close-tab-session-id={session.id}
title={isBusy ? "Removing..." : "Remove from Work view"}
className="ade-work-tab-close inline-flex items-center justify-center opacity-0 transition-opacity group-hover/tab:opacity-100 group-focus-within/tab:opacity-100 focus-visible:opacity-100"
style={{
🤖 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/terminals/WorkViewArea.tsx` around lines
1412 - 1417, The close button (data-close-tab-session-id, className
"ade-work-tab-close", title using isBusy) is only revealed on hover so keyboard
users cannot see it when focused; update the button's styling to also become
visible on keyboard focus by adding focus-visible and/or focus state classes
(e.g., focus:opacity-100 or focus-visible:opacity-100 and an appropriate focus
ring like focus:outline-none focus:ring) so tabbing shows the control and
maintains the existing hover behavior; ensure the change is applied to the same
button element inside WorkViewArea.tsx so keyboard users can access the "Remove
from Work view" action.


.ade-shell-project-tab {
--project-tab-accent: var(--color-accent);
height: var(--shell-project-tab-height);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix stylelint failures at Line 451 and Line 2716.

Both lines are failing declaration-empty-line-before; this can break lint-gated CI for the PR.

Also applies to: 2716-2716

🧰 Tools
🪛 Stylelint (17.12.0)

[error] 451-451: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 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/index.css` at line 451, Add a blank line before the
CSS declaration "height: var(--shell-project-tab-height);" (and the other
identical declaration flagged by stylelint) so the rule
declaration-empty-line-before is satisfied; if the declaration is the first
property in its ruleset, instead add a stylelint exemption comment
(stylelint-disable-next-line declaration-empty-line-before) immediately above
that declaration to avoid a false positive. Ensure you update both occurrences
flagged by the linter.

Comment thread apps/desktop/src/renderer/lib/linearIssueWorkNavigation.ts
Comment on lines +63 to +67
export function laneColorName(hex: string | null | undefined): string | null {
if (!hex) return null;
const lower = hex.toLowerCase();
return LANE_COLOR_PALETTE.find((entry) => entry.hex.toLowerCase() === lower)?.name ?? null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize hex consistently in laneColorName.

laneColorName doesn’t trim input, so whitespace-padded values won’t resolve even when valid.

💡 Suggested fix
 export function laneColorName(hex: string | null | undefined): string | null {
   if (!hex) return null;
-  const lower = hex.toLowerCase();
+  const lower = hex.trim().toLowerCase();
+  if (!lower) return null;
   return LANE_COLOR_PALETTE.find((entry) => entry.hex.toLowerCase() === lower)?.name ?? null;
 }
🤖 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/shared/laneColorPalette.ts` around lines 63 - 67, The
function laneColorName does not trim the input hex string, so whitespace-padded
values won't match; update laneColorName to first trim the incoming hex (e.g.,
const normalized = hex.trim()), return null if normalized is empty, then use
normalized.toLowerCase() when comparing against LANE_COLOR_PALETTE entries
(entry.hex.toLowerCase()) so trimmed values resolve correctly; keep using
LANE_COLOR_PALETTE and the existing find/?.name logic.

arul28 and others added 2 commits May 26, 2026 19:24
Refresh Work, Lanes, chat, and shell surfaces with shared lane/VCS chrome, add Linear quick-view resolve actions with confirmation modals and Work navigation, and land related dev/runtime helpers on the branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Desktop UI cleanup — simplify control flow, DRY repeated patterns, fix
Linear addedLabelIds mutation, update tests for new drawer selectors,
and align docs with orchestration rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28 arul28 force-pushed the ade/ui-clean-up-3470f34e branch from b904257 to 10e44ad Compare May 26, 2026 23:24
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 26, 2026

@copilot review but do not make fixes

Comment thread apps/desktop/src/renderer/lib/linearIssueWorkNavigation.ts
- Remove test for deliberately-removed Refresh button in UsageQuotaPanel
- Add 60s TTL to pending Linear issue work context to prevent stale consumption

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 26, 2026

@copilot review but do not make fixes

@arul28 arul28 merged commit 631d8fb into main May 27, 2026
28 of 29 checks passed
@arul28 arul28 deleted the ade/ui-clean-up-3470f34e branch May 27, 2026 04:45
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