Skip to content

Fix desktop release/update flow and improve terminal link handling - #55

Merged
AruNi-01 merged 19 commits into
mainfrom
fix/desktop_fix
Mar 20, 2026
Merged

AruNi-01 merged 19 commits into
mainfrom
fix/desktop_fix

Conversation

@AruNi-01

@AruNi-01 AruNi-01 commented Mar 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR stabilizes the desktop release/update pipeline, fixes desktop updater install behavior, and improves terminal link routing and related settings in the web app.

Related completed issue:

Feature

  • Add terminal smart link routing for project/workspace file paths, directories, external URLs, and common line/column formats.
  • Add terminal file-link open preferences in Settings with support for opening in Atmos, Finder, or a selected Quick Open app.
  • Add one-shot editor navigation targets so terminal links can open files directly at the referenced line and column.
  • Add directory reveal flow from terminal links into the Files tree.
  • Add a shared Quick Open app registry for header actions and terminal settings.
  • Move the right sidebar collapse control into the topbar and animate its visibility based on layout state.

Bugfix

  • Fix desktop updater install flow so Install reuses the checked update instead of silently no-oping.
  • Fix Settings update checks by replacing fragile popover-based result handling with toast-based feedback for up-to-date, available, download, install, and error states.
  • Fix desktop manual release workflow signing by exporting updater signing env vars and enabling ad-hoc signing for manual builds.
  • Fix desktop manual release workflow artifact collection paths and Node action compatibility.
  • Fix release workflow checks for manual desktop builds, ref validation, and matrix platform filtering.
  • Fix terminal internal file links failing to open in project-only contexts.
  • Fix terminal directory reveal behavior in the Files tree, including expansion, targeting, and highlight cleanup.
  • Fix workspace/sidebar hover controls, alignment, and hit areas.

Refactor

  • Consolidate terminal link parsing and activation logic into dedicated routing helpers.
  • Consolidate Quick Open app definitions into a shared module used by multiple UI surfaces.
  • Clean up sidebar/header control placement and related layout state wiring.
  • Minor layout consistency cleanup from the branch’s earlier refactor commit.

Validation

  • cd apps/web && bun typecheck

Notes

  • Desktop release/build changes were not end-to-end verified in CI from this PR body alone; they were implemented to address the observed workflow failures and signing issues.
  • Desktop updater UX changes are designed for Tauri runtime behavior; web builds remain safe no-ops for updater actions.

Summary by CodeRabbit

  • New Features

    • Terminal link opening preferences (choose Atmos, Finder, or a specific app) and quick-open app options.
    • Jump to a specific line/column in the editor.
    • Auto-reveal and transient highlight of files in the file tree.
  • Improvements

    • Update checks use toasts showing release notes and install progress with restart messaging.
    • Improved terminal link detection and project-relative path resolution.
    • Refined macOS window traffic-light positioning.

@vercel

vercel Bot commented Mar 20, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
atmos-landing Ready Ready Preview, Comment Mar 20, 2026 2:18pm

@coderabbitai

coderabbitai Bot commented Mar 20, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds terminal link routing with project-aware resolution and user preferences; editor navigation targets and file-tree reveal support; centralizes quick-open apps; moves right-sidebar toggle to Header; replaces update popover with toast flows and release notes links; tweaks macOS window positions; makes desktop release workflow platform-selectable with a dynamic matrix.

Changes

Cohort / File(s) Summary
CI / Release Workflow
.github/workflows/release-desktop.yml, scripts/release/check-desktop-version.mjs
Add platform workflow_dispatch input and dynamic build matrix via resolve-release-config; thread matrix into build-and-release; validate checkout_ref; split release vs non-release steps; collect artifacts for manual builds; remove fallback for release-tag.
Tauri macOS config
apps/desktop/src-tauri/tauri.conf.json, apps/desktop/src-tauri/tauri.debug.conf.json
Adjust macOS trafficLightPosition.y from 26 → 24 for splashscreen and main windows.
Terminal: routing & components
apps/web/src/components/terminal/terminal-link-routing.ts, apps/web/src/components/terminal/Terminal.tsx, apps/web/src/components/terminal/TerminalGrid.tsx, apps/web/src/components/terminal/types.ts
Replace xterm web-links addon with custom resolver/provider; add token parsing, projectRootPath-scoped resolution, click interception, modifier-key gating, and pass project root into Terminal panes.
Terminal settings & updater
apps/web/src/hooks/use-terminal-link-settings.ts, apps/web/src/hooks/use-updater.ts
New Zustand hook for terminal file-link open mode/app (load/save/validation); updater adds release-notes URL helper, pendingUpdate caching, and improved download progress & install guards.
Settings UI & Updater flow
apps/web/src/components/dialogs/SettingsModal.tsx, apps/web/src/components/layout/UpdateNotification.tsx
Replace update popover with toast-driven check/install flow; add Install flow states, release notes link, and new Terminal settings section with preference controls.
Editor navigation targets
apps/web/src/components/editor/BaseCodeMirrorEditor.tsx, apps/web/src/components/editor/CodeMirrorEditor.tsx, apps/web/src/hooks/use-editor-store.ts
Add per-file navigationTargets in store and APIs to set/clear them; editor accepts navigationTarget prop and applies single-cursor selection + centered scroll; CodeMirror wiring to clear target after apply.
File tree reveal & sidebar
apps/web/src/components/files/FileTree.tsx, apps/web/src/components/layout/LeftSidebar.tsx
Implement file-tree reveal flow: expand path, focus & timed highlight driven by fileTreeRevealTarget; LeftSidebar auto-switches to files tab when appropriate.
Quick-open apps centralization
apps/web/src/components/layout/quick-open-apps.tsx, apps/web/src/components/layout/QuickOpen.tsx
Add shared quick-open app data/component (QUICK_OPEN_APP_*, QuickOpenAppIcon); refactor QuickOpen to use grouped dynamic app lists and validated selection state.
Layout & sidebar toggles
apps/web/src/components/layout/Header.tsx, apps/web/src/components/layout/CenterStage.tsx, apps/web/src/components/layout/PanelLayout.tsx, apps/web/src/components/layout/sidebar/WorkspaceContent.tsx
Move right-sidebar toggle into Header with AnimatePresence; fix toggle callback shape in PanelLayout; remove right toggle from CenterStage; WorkspaceContent visual/layout adjustments and hover action styling changes.
Web API types
apps/web/src/api/ws-api.ts
Extend exported FunctionSettings with optional terminal object (file_link_open_mode, file_link_open_app).
Misc / Gitignore
.gitignore
Add .junie/ to ignore patterns.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Terminal
    participant Routing as terminal-link-routing
    participant FS as fsApi
    participant Store as useEditorStore
    participant Editor as CodeMirrorEditor

    User->>Terminal: Click/activate link
    Terminal->>Routing: resolveTerminalLink(token, { projectRootPath })
    Routing->>FS: probe candidates (readFile/listDir/getHomeDir)
    FS-->>Routing: resolution (file / dir / external / null)
    Routing-->>Terminal: ResolvedTerminalLink
    Terminal->>Store: openFile(path, workspaceId, { line, column })
    Terminal->>Store: requestFileTreeReveal(path, workspaceId)
    Store-->>Editor: set navigationTarget for file
    Editor->>Editor: apply selection & scrollIntoView
Loading
sequenceDiagram
    actor User
    participant Terminal
    participant Store as useEditorStore
    participant LeftSidebar
    participant FileTree
    participant FS as fsApi

    User->>Terminal: Open file link
    Terminal->>Store: openFile(...) & requestFileTreeReveal(...)
    Store->>LeftSidebar: fileTreeRevealTarget updated
    LeftSidebar->>LeftSidebar: switch to 'files' tab (if needed)
    LeftSidebar->>FileTree: reveal target path
    FileTree->>FS: listDir / load children along path
    FileTree->>User: focus + timed highlight
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hopped through tokens, paths in tow,
Mapped links to files where wild clues grow,
Trees unfolded, toasts sang bright,
Quick-open apps aligned just right,
A tiny hop — the workflow’s set to go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main focus of the changeset: fixing desktop release/update flow and improving terminal link handling.
Linked Issues check ✅ Passed The PR addresses issue #51 requirements: fixes Install button no-op by reusing pending updates, replaces popover with toast-based feedback for update states, and provides proper UI state management for available/downloading/installing/error states.
Out of Scope Changes check ✅ Passed Changes are scope-appropriate: release workflow improvements and environment variable export support the desktop build/install fixes; terminal link routing, settings UI, and Quick Open consolidation directly enable terminal link handling; layout refactoring supports sidebar/header restructuring noted in objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/desktop_fix
📝 Coding Plan
  • Generate coding plan for human review comments

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

❤️ Share

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

devin-ai-integration[bot]

This comment was marked as resolved.

🟡 Release notes URL uses wrong tag prefix, generating broken links

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
cubic-dev-ai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
apps/web/src/hooks/use-updater.ts (1)

55-65: 🛠️ Refactor suggestion | 🟠 Major

Move the Tauri updater/process I/O behind src/api/.

This change grows the shared updater state here, but the module still owns the actual check, downloadAndInstall, and relaunch calls. Please move that I/O into an apps/web/src/api/* module and keep use-updater.ts focused on client-side status orchestration.

As per coding guidelines, "All API interaction logic must live in src/api/" and "Use hooks/ directory for client-side state logic".

Also applies to: 88-99, 113-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/use-updater.ts` around lines 55 - 65, Move all Tauri I/O
out of the hook into an API module: create an apps/web/src/api/updater.ts that
exports async wrapper functions (e.g., checkForUpdateIO/checkUpdate,
downloadAndInstallUpdate, relaunchApp) which internally import
'@tauri-apps/plugin-updater' and invoke the actual check(), install/download,
and relaunch calls (and handle isTauriRuntime guard inside those wrappers). In
use-updater.ts keep the client-side orchestration (the exported checkForUpdate
hook function and any status callbacks) but replace direct calls to the Tauri
plugin with calls to the new api functions (preserve the same
UpdateInfo/UpdateStatus types and function names used in the hook), and ensure
fallbacks/null returns when not running in Tauri.
apps/web/src/components/agent/AgentManagerView.tsx (1)

277-309: ⚠️ Potential issue | 🟠 Major

Filter custom agents before including them in Installed.

This view now mixes mgr.installedAgents with mgr.customAgents, and the latter still appears to be the raw list because it also drives the tab counts. That means a search that matches no installed agents will still render every custom agent here, and the No installed agents matching... state never appears once any custom agent exists. Please render a query-filtered custom list in this tab as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/agent/AgentManagerView.tsx` around lines 277 - 309,
The tab is rendering unfiltered mgr.customAgents alongside mgr.installedAgents,
so search queries still show all custom agents; filter the custom agents the
same way as installed ones before rendering and counting: create a
filteredCustom list (e.g., const filteredCustom = mgr.customAgents.filter(a =>
matchesQuery(a, query)) or reuse the existing installed-agent filtering
predicate), replace uses of mgr.customAgents in the AgentManagerView render with
filteredCustom (map to <CustomAgentCard ... index={mgr.installedAgents.length +
i} ...>), and update any tab-count logic that references mgr.customAgents to use
filteredCustom.length so the "No installed agents matching..." empty state
appears correctly.
🧹 Nitpick comments (4)
.github/workflows/release-desktop.yml (1)

257-272: Consider using env vars for safer secret handling.

Direct interpolation of ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} into the shell condition (line 262) is fragile if the secret contains shell metacharacters. Additionally, the hardcoded heredoc delimiter __EOF__ would break if the secret itself contains that string.

♻️ Safer pattern using environment variables
       - name: Export updater signing env
         shell: bash
+        env:
+          _SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
+          _SIGNING_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
         run: |
           set -euo pipefail

-          if [ -z "${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}" ]; then
+          if [ -z "$_SIGNING_KEY" ]; then
             echo "❌ Missing secret: TAURI_SIGNING_PRIVATE_KEY"
             exit 1
           fi

+          DELIMITER="__EOF_$(date +%s)__"
           {
-            echo 'TAURI_SIGNING_PRIVATE_KEY<<__EOF__'
-            echo "${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}"
-            echo '__EOF__'
-            echo "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}"
+            echo "TAURI_SIGNING_PRIVATE_KEY<<$DELIMITER"
+            echo "$_SIGNING_KEY"
+            echo "$DELIMITER"
+            echo "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$_SIGNING_KEY_PASSWORD"
           } >> "$GITHUB_ENV"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-desktop.yml around lines 257 - 272, The step
"Export updater signing env" currently interpolates `${{
secrets.TAURI_SIGNING_PRIVATE_KEY }}` directly into the shell which can break
with shell metacharacters or if the secret contains the `__EOF__` delimiter;
instead, pass TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD
into the step via the step's env: mapping (e.g. env: TAURI_SIGNING_PRIVATE_KEY:
${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}), check the presence using the shell
variable `$TAURI_SIGNING_PRIVATE_KEY`, and write them into GITHUB_ENV safely
(e.g. avoid brittle heredoc delimiters by encoding the key (base64) or using a
delimiter generated at runtime) so the variables TAURI_SIGNING_PRIVATE_KEY and
TAURI_SIGNING_PRIVATE_KEY_PASSWORD are exported without risk of shell injection
or delimiter collision.
apps/web/src/components/terminal/Terminal.tsx (1)

1020-1020: Including projectRootPath in dependencies causes unnecessary terminal recreation.

When projectRootPath changes, the entire terminal is torn down and recreated, disconnecting the WebSocket and losing any in-progress session state. However, projectRootPath only affects link resolution in the link provider.

Consider extracting the link provider registration into a separate effect that can update without destroying the terminal, or use a ref to hold projectRootPath for the link handlers.

♻️ Alternative approach using ref
+const projectRootPathRef = useRef(projectRootPath);
+useEffect(() => {
+  projectRootPathRef.current = projectRootPath;
+}, [projectRootPath]);

 // In createTerminalLinkProvider call:
-createTerminalLinkProvider(terminal, { projectRootPath }, (event, target) => {
+createTerminalLinkProvider(terminal, { get projectRootPath() { return projectRootPathRef.current; } }, (event, target) => {

 // Remove projectRootPath from the main effect's dependencies
-}, [sessionId, workspaceId, cwd, projectRootPath]);
+}, [sessionId, workspaceId, cwd]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/terminal/Terminal.tsx` at line 1020, The effect that
recreates the terminal (the useEffect whose dependency array includes sessionId,
workspaceId, cwd, projectRootPath) is tearing down the terminal when
projectRootPath changes; move link provider registration out of that effect so
changes to projectRootPath don't recreate the terminal. Specifically, keep the
terminal creation/cleanup logic in the existing effect but remove any
registerLinkProvider/registerLinkHandlers code from it and create a second
useEffect (or store projectRootPath in a ref via useRef and read the ref inside
the link handler) that solely updates the link provider when projectRootPath
changes; update registerLinkProvider/registerLinkHandlers to read
projectRootPath from the ref (or be run by the separate effect) so the
WebSocket/terminal instance (terminal, socket, etc.) isn't recreated on
projectRootPath updates.
apps/web/src/components/terminal/terminal-link-routing.ts (1)

263-273: Consider a lightweight existence check instead of reading file content.

fsApi.readFile(candidatePath) reads the entire file content just to determine if it exists. For large files, this is wasteful. If the API supports it, a stat or exists check would be more efficient.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/terminal/terminal-link-routing.ts` around lines 263 -
273, The code currently calls fsApi.readFile(candidatePath) inside the lookup
loop (see candidatePath handling) just to detect existence; replace that with a
lightweight existence/stat check (e.g., fsApi.stat, fsApi.exists, or an
fsApi.access-style method if provided) so we don't load file contents for large
files, and update the result handling to check existence/nullability from that
API (returning the same { type: "file", path: candidatePath } when it exists).
Keep the try/catch behavior to ignore lookup failures and fallback to other
candidates.
apps/web/src/components/agent/AgentManagerView.tsx (1)

51-53: Drop the unnecessary useCallback wrapper on handleCustomDialogSaved.

The mgr.loadData callback is already memoized in the useAgentManager hook (has empty dependency array), so wrapping it in another useCallback adds no value. Simplify to a direct arrow function:

const handleCustomDialogSaved = () => {
  void mgr.loadData();
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/agent/AgentManagerView.tsx` around lines 51 - 53,
Remove the unnecessary React.useCallback wrapper around handleCustomDialogSaved:
replace the memoized wrapper with a plain arrow function that calls
mgr.loadData() directly (keep using void mgr.loadData() to preserve the original
async intent). Update the declaration of handleCustomDialogSaved (referenced in
AgentManagerView.tsx) to be a simple const handleCustomDialogSaved = () => {
void mgr.loadData(); }; and remove useCallback import if it becomes unused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/agent/AgentManagerView.tsx`:
- Around line 216-223: The Installed badge currently hardcodes emerald color
classes inside the TabsTrigger block (see the span rendering the count using
mgr.installedCount + mgr.customAgents.length); replace those hardcoded classes
with the app's semantic theme tokens (e.g., use bg-[semantic-token] /
text-[semantic-token] / border-[semantic-token] equivalents such as
bg-background variants, text-muted-foreground or the project's semantic emerald
tokens and border-border) so the badge respects light/dark theming and the
project's style guidelines; update the span's className accordingly while
preserving sizing, rounding and font rules.

In `@apps/web/src/components/terminal/Terminal.tsx`:
- Around line 713-731: Replace the private xterm.js access (the use of
terminal._core/_mouseService/getCoords in Terminal.tsx) with a calculation using
public APIs: attach your mouse listener to the terminal.element (or viewport),
get the element bounding rect via getBoundingClientRect(), read cell
width/height from terminal.renderer.dimensions.css.cell, compute column =
floor((clientX - rect.left) / cell.width) and row = floor((clientY - rect.top) /
cell.height) and add terminal.buffer.active.ydisp to the row to account for
scroll; optionally cache terminal.renderer.dimensions.css.cell to avoid repeated
measurements for performance.

In `@apps/web/src/hooks/use-updater.ts`:
- Around line 92-95: The current early return when isInstallingUpdate is true
silently drops callers; change the check in the installer function in
use-updater.ts (the block using isInstallingUpdate) to return an explicit
in-progress result or re-play the current updater status to the caller instead
of a bare return. Concretely, replace "if (isInstallingUpdate) return;" with a
branch that either rejects/returns a Promise with a clearly typed status (e.g.
Promise.resolve({ status: 'in-progress' }) or Promise.reject(new
Error('update-in-progress'))) or returns the same status object/state_ref the
hook exposes so callers (e.g. the SettingsModal and UpdateNotification
consumers) can observe and react to the updater being busy. Ensure the returned
value matches the hook's existing return type so callers can handle the
in-progress signal.

---

Outside diff comments:
In `@apps/web/src/components/agent/AgentManagerView.tsx`:
- Around line 277-309: The tab is rendering unfiltered mgr.customAgents
alongside mgr.installedAgents, so search queries still show all custom agents;
filter the custom agents the same way as installed ones before rendering and
counting: create a filteredCustom list (e.g., const filteredCustom =
mgr.customAgents.filter(a => matchesQuery(a, query)) or reuse the existing
installed-agent filtering predicate), replace uses of mgr.customAgents in the
AgentManagerView render with filteredCustom (map to <CustomAgentCard ...
index={mgr.installedAgents.length + i} ...>), and update any tab-count logic
that references mgr.customAgents to use filteredCustom.length so the "No
installed agents matching..." empty state appears correctly.

In `@apps/web/src/hooks/use-updater.ts`:
- Around line 55-65: Move all Tauri I/O out of the hook into an API module:
create an apps/web/src/api/updater.ts that exports async wrapper functions
(e.g., checkForUpdateIO/checkUpdate, downloadAndInstallUpdate, relaunchApp)
which internally import '@tauri-apps/plugin-updater' and invoke the actual
check(), install/download, and relaunch calls (and handle isTauriRuntime guard
inside those wrappers). In use-updater.ts keep the client-side orchestration
(the exported checkForUpdate hook function and any status callbacks) but replace
direct calls to the Tauri plugin with calls to the new api functions (preserve
the same UpdateInfo/UpdateStatus types and function names used in the hook), and
ensure fallbacks/null returns when not running in Tauri.

---

Nitpick comments:
In @.github/workflows/release-desktop.yml:
- Around line 257-272: The step "Export updater signing env" currently
interpolates `${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}` directly into the shell
which can break with shell metacharacters or if the secret contains the
`__EOF__` delimiter; instead, pass TAURI_SIGNING_PRIVATE_KEY and
TAURI_SIGNING_PRIVATE_KEY_PASSWORD into the step via the step's env: mapping
(e.g. env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}),
check the presence using the shell variable `$TAURI_SIGNING_PRIVATE_KEY`, and
write them into GITHUB_ENV safely (e.g. avoid brittle heredoc delimiters by
encoding the key (base64) or using a delimiter generated at runtime) so the
variables TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD are
exported without risk of shell injection or delimiter collision.

In `@apps/web/src/components/agent/AgentManagerView.tsx`:
- Around line 51-53: Remove the unnecessary React.useCallback wrapper around
handleCustomDialogSaved: replace the memoized wrapper with a plain arrow
function that calls mgr.loadData() directly (keep using void mgr.loadData() to
preserve the original async intent). Update the declaration of
handleCustomDialogSaved (referenced in AgentManagerView.tsx) to be a simple
const handleCustomDialogSaved = () => { void mgr.loadData(); }; and remove
useCallback import if it becomes unused.

In `@apps/web/src/components/terminal/terminal-link-routing.ts`:
- Around line 263-273: The code currently calls fsApi.readFile(candidatePath)
inside the lookup loop (see candidatePath handling) just to detect existence;
replace that with a lightweight existence/stat check (e.g., fsApi.stat,
fsApi.exists, or an fsApi.access-style method if provided) so we don't load file
contents for large files, and update the result handling to check
existence/nullability from that API (returning the same { type: "file", path:
candidatePath } when it exists). Keep the try/catch behavior to ignore lookup
failures and fallback to other candidates.

In `@apps/web/src/components/terminal/Terminal.tsx`:
- Line 1020: The effect that recreates the terminal (the useEffect whose
dependency array includes sessionId, workspaceId, cwd, projectRootPath) is
tearing down the terminal when projectRootPath changes; move link provider
registration out of that effect so changes to projectRootPath don't recreate the
terminal. Specifically, keep the terminal creation/cleanup logic in the existing
effect but remove any registerLinkProvider/registerLinkHandlers code from it and
create a second useEffect (or store projectRootPath in a ref via useRef and read
the ref inside the link handler) that solely updates the link provider when
projectRootPath changes; update registerLinkProvider/registerLinkHandlers to
read projectRootPath from the ref (or be run by the separate effect) so the
WebSocket/terminal instance (terminal, socket, etc.) isn't recreated on
projectRootPath updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89228bf1-c74d-4122-a4fc-c5b0fb0eed50

📥 Commits

Reviewing files that changed from the base of the PR and between ff2be10 and 9211381.

📒 Files selected for processing (15)
  • .github/workflows/release-desktop.yml
  • apps/web/src/components/agent/AgentManagerView.tsx
  • apps/web/src/components/dialogs/SettingsModal.tsx
  • apps/web/src/components/editor/CodeMirrorEditor.tsx
  • apps/web/src/components/files/FileTree.tsx
  • apps/web/src/components/layout/CenterStage.tsx
  • apps/web/src/components/layout/LeftSidebar.tsx
  • apps/web/src/components/layout/QuickOpen.tsx
  • apps/web/src/components/layout/quick-open-apps.tsx
  • apps/web/src/components/terminal/Terminal.tsx
  • apps/web/src/components/terminal/TerminalGrid.tsx
  • apps/web/src/components/terminal/terminal-link-routing.ts
  • apps/web/src/hooks/use-editor-store.ts
  • apps/web/src/hooks/use-terminal-link-settings.ts
  • apps/web/src/hooks/use-updater.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/web/src/components/layout/LeftSidebar.tsx
  • apps/web/src/components/editor/CodeMirrorEditor.tsx
  • apps/web/src/hooks/use-terminal-link-settings.ts
  • apps/web/src/hooks/use-editor-store.ts
  • apps/web/src/components/terminal/TerminalGrid.tsx
  • apps/web/src/components/layout/QuickOpen.tsx
  • apps/web/src/components/files/FileTree.tsx
  • apps/web/src/components/layout/quick-open-apps.tsx
  • apps/web/src/components/dialogs/SettingsModal.tsx

Comment on lines +216 to +223
<TabsTrigger value="installed">
<Download className="size-4" />
Installed
{!mgr.loading && mgr.installedCount + mgr.customAgents.length > 0 && (
<span className="ml-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 px-1.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400 tabular-nums">
{mgr.installedCount + mgr.customAgents.length}
</span>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid hardcoded palette classes in the new Installed badge.

The added badge styling hardcodes emerald-* values instead of the shared theme tokens used elsewhere in this view. Please switch this to semantic color classes so it stays aligned in both light and dark themes.

As per coding guidelines "ALWAYS use semantic CSS variables (bg-background, text-muted-foreground, border-border, etc.) instead of hardcoded Tailwind colors like bg-zinc-900 or text-gray-500 for layout components".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/agent/AgentManagerView.tsx` around lines 216 - 223,
The Installed badge currently hardcodes emerald color classes inside the
TabsTrigger block (see the span rendering the count using mgr.installedCount +
mgr.customAgents.length); replace those hardcoded classes with the app's
semantic theme tokens (e.g., use bg-[semantic-token] / text-[semantic-token] /
border-[semantic-token] equivalents such as bg-background variants,
text-muted-foreground or the project's semantic emerald tokens and
border-border) so the badge respects light/dark theming and the project's style
guidelines; update the span's className accordingly while preserving sizing,
rounding and font rules.

Comment on lines +713 to +731
const core = (terminal as unknown as {
_core?: {
_mouseService?: {
getCoords: (
event: MouseEvent,
element: HTMLElement,
colCount: number,
rowCount: number,
) => [number, number] | undefined;
};
};
})._core;

const coords = core?._mouseService?.getCoords(
event,
terminal.element ?? container,
terminal.cols,
terminal.rows,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

xterm.js get mouse coordinates from click event public API

💡 Result:

xterm.js does not provide a public API method to directly get grid (row/col) coordinates from a raw DOM MouseEvent. Mouse events are primarily handled internally for terminal protocol reporting (e.g., CSI sequences for backend apps like vim/tmux), not exposed as public events with coordinates. To get mouse coordinates from a click event using public APIs: 1. Attach a listener to the viewport element: term.element.querySelector('.xterm-viewport') or term.viewport.viewportElement (noted as non-public in older docs but accessible). 2. In the handler, compute relative position using getBoundingClientRect and event clientX/clientY. 3. Convert pixel offsets to grid coordinates using terminal dimensions: term.renderer.dimensions.css.cell.width and .cell.height for col/row. Example code: const term = new Terminal; term.open(document.getElementById('terminal')); const viewport = term.element.querySelector('.xterm-viewport'); viewport.addEventListener('click', (e) => { const rect = viewport.getBoundingClientRect; const x = e.clientX - rect.left; const y = e.clientY - rect.top; const cellWidth = term.renderer.dimensions.css.cell.width; const cellHeight = term.renderer.dimensions.css.cell.height; const col = Math.floor(x / cellWidth); const row = Math.floor(y / cellHeight) + term.buffer.active.ydisp; // ydisp for absolute buffer row console.log(Clicked at buffer row ${row}, col ${col}); }); Notes: - ydisp accounts for scroll position to get absolute buffer coordinates. - Coordinates are 0-based. - This matches internal logic (e.g., getBoundingClientRect used in selection/mouse handling). - No dedicated public onMouseClick or IMouseEvent API exists (proposed but not implemented). - For selection-related positions, use term.getSelectionPosition which returns IBufferRange with start/end {start: {x, y}, end: {x, y}} (viewport-relative y). This approach is stable, as element, renderer.dimensions, and buffer.ydisp are public.

Citations:


Replace private xterm.js API with public alternatives for stability.

The code accesses terminal._core._mouseService.getCoords(), a private internal API with no public equivalent in xterm.js. While xterm.js provides no dedicated public API for getting grid coordinates from mouse events, you can achieve this using stable public APIs:

Use term.element, term.renderer.dimensions.css.cell, and term.buffer.active.ydisp to manually calculate coordinates instead:

  • Attach listener to viewport element
  • Get pixel offsets via getBoundingClientRect()
  • Divide by cell dimensions to get grid position
  • Add ydisp to account for scroll position

This approach avoids private API fragility while remaining stable across xterm.js versions. If performance is critical, consider caching cell dimensions to reduce recalculation overhead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/terminal/Terminal.tsx` around lines 713 - 731,
Replace the private xterm.js access (the use of
terminal._core/_mouseService/getCoords in Terminal.tsx) with a calculation using
public APIs: attach your mouse listener to the terminal.element (or viewport),
get the element bounding rect via getBoundingClientRect(), read cell
width/height from terminal.renderer.dimensions.css.cell, compute column =
floor((clientX - rect.left) / cell.width) and row = floor((clientY - rect.top) /
cell.height) and add terminal.buffer.active.ydisp to the row to account for
scroll; optionally cache terminal.renderer.dimensions.css.cell to avoid repeated
measurements for performance.

Comment on lines +92 to +95
if (isInstallingUpdate) return;

try {
isInstallingUpdate = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t silently drop callers when an install is already running.

Line 92 returns without notifying the caller, so other entry points never learn that the updater is busy. apps/web/src/components/dialogs/SettingsModal.tsx at Lines 101-177 only clears installInFlightRef.current from callback stages, and apps/web/src/components/layout/UpdateNotification.tsx at Lines 30-38 disables its button from the same status stream. Replay the current updater status here, or return an explicit in-progress result, instead of a silent no-op.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/use-updater.ts` around lines 92 - 95, The current early
return when isInstallingUpdate is true silently drops callers; change the check
in the installer function in use-updater.ts (the block using isInstallingUpdate)
to return an explicit in-progress result or re-play the current updater status
to the caller instead of a bare return. Concretely, replace "if
(isInstallingUpdate) return;" with a branch that either rejects/returns a
Promise with a clearly typed status (e.g. Promise.resolve({ status:
'in-progress' }) or Promise.reject(new Error('update-in-progress'))) or returns
the same status object/state_ref the hook exposes so callers (e.g. the
SettingsModal and UpdateNotification consumers) can observe and react to the
updater being busy. Ensure the returned value matches the hook's existing return
type so callers can handle the in-progress signal.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +100 to +138
const loadDirectoryChildren = useCallback(async (itemPath: string): Promise<string[]> => {
const existingItem = initialItemsMap.get(itemPath) || lazyItemsMap.get(itemPath);
if (!existingItem?.isDir) return [];

if (existingItem.children && existingItem.children.length > 0) {
return existingItem.children;
}

const response = await fsApi.listDir(itemPath, { showHidden: true, dirsOnly: false });

const newChildren = response.entries.map((entry) => entry.path);
const newEntriesMap = new Map<string, FileTreeItem>();

response.entries.forEach((entry) => {
newEntriesMap.set(entry.path, {
id: entry.path,
name: entry.name,
path: entry.path,
isDir: entry.is_dir,
isSymlink: entry.is_symlink,
isIgnored: entry.is_ignored,
symlinkTarget: entry.symlink_target,
});
});

setLazyItemsMap((prev: Map<string, FileTreeItem>) => {
const next = new Map(prev);
newEntriesMap.forEach((val, key) => next.set(key, val));

const parent = initialItemsMap.get(itemPath) || next.get(itemPath);
if (parent) {
next.set(itemPath, { ...parent, children: newChildren });
}

return next;
});

return newChildren;
}, [initialItemsMap, lazyItemsMap]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 loadDirectoryChildren dependency on lazyItemsMap causes reveal effect to restart on each directory load

loadDirectoryChildren includes lazyItemsMap in its useCallback dependency array (FileTree.tsx:138). Every call to loadDirectoryChildren updates lazyItemsMap via setLazyItemsMap (FileTree.tsx:125), which creates a new Map, causing the callback reference to change. Since loadDirectoryChildren is in the reveal effect's dependency array (FileTree.tsx:283), the effect restarts on each directory load.

Within a single effect execution, when the loop calls loadDirectoryChildren for a parent directory, the resulting setLazyItemsMap update hasn't been committed yet. The next iteration tries to load a child directory, but the stale lazyItemsMap closure doesn't contain the just-loaded children — so existingItem is undefined and the function returns [] for a valid directory. The effect then gets cancelled when React processes the batched state update and restarts from scratch. For a path N levels deep, this requires ~N restarts, each re-traversing from the root.

Prompt for agents
In apps/web/src/components/files/FileTree.tsx, the loadDirectoryChildren useCallback at line 100-138 depends on lazyItemsMap (line 138), which changes every time a directory is lazily loaded. This causes the callback reference to change on each load, triggering the reveal effect (line 206-283) to restart.

Fix: Use a ref to access lazyItemsMap inside the callback instead of capturing it in the closure. Add a ref like:
  const lazyItemsMapRef = useRef(lazyItemsMap);
  useEffect(() => { lazyItemsMapRef.current = lazyItemsMap; }, [lazyItemsMap]);

Then in loadDirectoryChildren, replace:
  const existingItem = initialItemsMap.get(itemPath) || lazyItemsMap.get(itemPath);
with:
  const existingItem = initialItemsMap.get(itemPath) || lazyItemsMapRef.current.get(itemPath);

And change the dependency array from [initialItemsMap, lazyItemsMap] to [initialItemsMap].

This keeps the callback reference stable while still reading the latest lazyItemsMap, preventing the reveal effect from restarting on each directory load.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@AruNi-01
AruNi-01 merged commit 50ab405 into main Mar 20, 2026
11 of 14 checks passed

This branch was successfully deployed

1 active deployment
Preview — 92113817 Deployed Mar 20, 2026 by vercel[bot]
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.

Desktop App version update invalid

1 participant