Skip to content

Shell: cross-app drag-drop primitive + Files → Messages wiring#239

Merged
jaylfc merged 1 commit into
masterfrom
feat/shell-cross-app-dnd
Apr 20, 2026
Merged

Shell: cross-app drag-drop primitive + Files → Messages wiring#239
jaylfc merged 1 commit into
masterfrom
feat/shell-cross-app-dnd

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Apr 20, 2026

Summary

Lands the shell-wide drag-drop primitive (#60): a typed payload bus + useDragSource / useDropTarget hooks. First integration wires FilesApp file rows as drag sources and the MessagesApp message list as a drop target that turns workspace files into chat attachments.

Primitive

  • desktop/src/shell/dnd/types.ts — discriminated union `DragPayload = file | message | knowledge | canvas-block`.
  • desktop/src/shell/dnd/dnd-bus.ts — singleton event-emitter bus with 30s stale-drag safety timeout.
  • desktop/src/shell/dnd/use-drag-source.ts — spreads `draggable` + drag handlers; writes HTML5 `dataTransfer.setData` mirror for OS-level drags.
  • desktop/src/shell/dnd/use-drop-target.ts — subscribes to the bus, exposes `isOver` / `isValidTarget` with accept-filter; enter-counter handles nested children correctly.

Wiring

  • FilesApp: file rows (not folders) become drag sources. Payload carries the VFS path (`/workspaces/user/...` or `/workspaces/agent//...`) + mime + size. HTML5 mirror puts the path in `text/plain` for external drops.
  • MessagesApp: message list is a drop target accepting `kind: "file"`. On drop, calls the existing `attachmentFromPath` API to turn the VFS path into a chat attachment; pending bar shows the chip. OS-level file drops (dragging from Finder/Explorer) retain their existing flow unchanged.

Test plan

  • 14 new vitest unit tests (bus 5, useDragSource 4, useDropTarget 5). All pass.
  • Full desktop suite: 278/281 (3 pre-existing snap-zones failures unrelated).
  • Build clean.
  • Playwright E2E stub written (`tests/e2e/test_cross_app_dnd.py`); gated on `TAOS_E2E_URL`.
  • Manual smoke: open Files + Messages, drag a file row onto the message list, verify attachment chip appears and send works.

Follow-ups (not in this PR)

  • Messages source → TextEditor target (quote-into-notes).
  • LibraryApp source wiring.
  • CanvasApp source + target.
  • Touch drag (long-press) on mobile — requires shell pointer-event adapter; deferred with mobile PWA polish.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added cross-app drag-and-drop support: users can now drag files from the Files app and drop them directly into the Messages composer to attach files.
    • Enhanced file attachment workflow with improved drag-and-drop interaction.
  • Documentation

    • Added design specification and implementation plan for cross-app drag-and-drop architecture.
  • Tests

    • Added comprehensive test coverage for drag-and-drop functionality and bus state management.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Warning

Rate limit exceeded

@jaylfc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 36 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 36 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 595c03d6-b29b-40fe-bdcd-d6314777968c

📥 Commits

Reviewing files that changed from the base of the PR and between 40adaac and 7873947.

📒 Files selected for processing (69)
  • desktop/src/apps/FilesApp.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/shell/dnd/__tests__/dnd-bus.test.ts
  • desktop/src/shell/dnd/__tests__/use-drag-source.test.tsx
  • desktop/src/shell/dnd/__tests__/use-drop-target.test.tsx
  • desktop/src/shell/dnd/dnd-bus.ts
  • desktop/src/shell/dnd/types.ts
  • desktop/src/shell/dnd/use-drag-source.ts
  • desktop/src/shell/dnd/use-drop-target.ts
  • desktop/tsconfig.tsbuildinfo
  • docs/superpowers/plans/2026-04-20-shell-cross-app-dnd.md
  • docs/superpowers/specs/2026-04-20-shell-cross-app-dnd-design.md
  • static/desktop/assets/ActivityApp-B9wIvJAK.js
  • static/desktop/assets/AgentBrowsersApp-Ch7Kx-O4.js
  • static/desktop/assets/AgentsApp-BZw6W6Vy.js
  • static/desktop/assets/BrowserApp-KbremVMB.js
  • static/desktop/assets/CalendarApp-DZD5-sDA.js
  • static/desktop/assets/ChannelsApp-BveF_Oog.js
  • static/desktop/assets/ClusterApp-oYzyi0Dp.js
  • static/desktop/assets/ContactsApp-DDczp9Hc.js
  • static/desktop/assets/FilesApp-Bm-rxwrE.js
  • static/desktop/assets/FilesApp-DpnvIlme.js
  • static/desktop/assets/GitHubApp-B8-CS8Px.js
  • static/desktop/assets/ImageViewerApp-IGHKuKLt.js
  • static/desktop/assets/ImagesApp-DrJsImHG.js
  • static/desktop/assets/ImportApp-CB2pqwOR.js
  • static/desktop/assets/ImportApp-DBAV17Xb.js
  • static/desktop/assets/LibraryApp-BD1Wtjm7.js
  • static/desktop/assets/MCPApp-Ds2pyPo2.js
  • static/desktop/assets/MemoryApp-Dw5harOM.js
  • static/desktop/assets/MessagesApp-D7Umpsei.js
  • static/desktop/assets/MobileSplitView-B-4MQO2C.js
  • static/desktop/assets/ModelsApp-BGQXnEI3.js
  • static/desktop/assets/ProvidersApp-BEdnJTlt.js
  • static/desktop/assets/RedditApp-CVpnnpBP.js
  • static/desktop/assets/SecretsApp-Dq-GKn40.js
  • static/desktop/assets/SettingsApp-3EqaoTva.js
  • static/desktop/assets/StoreApp-B9C6ujYf.js
  • static/desktop/assets/TasksApp-CcqxzxyH.js
  • static/desktop/assets/TextEditorApp-DX1ldFmW.js
  • static/desktop/assets/XApp-CKMiMFvk.js
  • static/desktop/assets/YouTubeApp-CAVU-1ZB.js
  • static/desktop/assets/chat-CnrplGIN.js
  • static/desktop/assets/dnd-bus-CdEoCWzR.js
  • static/desktop/assets/index-BEv-dcGk.js
  • static/desktop/assets/index-BlduOVkv.js
  • static/desktop/assets/index-BqnE0gGN.js
  • static/desktop/assets/index-Ce1hN6V-.js
  • static/desktop/assets/index-ClPK3Mg_.js
  • static/desktop/assets/index-CniRZ7Tm.js
  • static/desktop/assets/index-Crp4FTaY.js
  • static/desktop/assets/index-DGMKK4xB.js
  • static/desktop/assets/index-DMMUxG_R.js
  • static/desktop/assets/index-DTZBLne8.js
  • static/desktop/assets/index-DU8P0UT_.js
  • static/desktop/assets/index-DUWvMC11.js
  • static/desktop/assets/index-DuDKTnwy.js
  • static/desktop/assets/index-DwTu_mTz.js
  • static/desktop/assets/index-ESMkhiX0.js
  • static/desktop/assets/index-HCqzTdtf.js
  • static/desktop/assets/index-OlIDYpbO.js
  • static/desktop/assets/main-w-WDDs5-.js
  • static/desktop/assets/tokens-BKHlNYpw.js
  • static/desktop/assets/tokens-DbWedNtZ.css
  • static/desktop/assets/vendor-codemirror-aNcHammg.js
  • static/desktop/assets/vendor-icons--yQaiqiL.js
  • static/desktop/chat.html
  • static/desktop/index.html
  • tests/e2e/test_cross_app_dnd.py
📝 Walkthrough

Walkthrough

This PR introduces a cross-app drag-and-drop primitive layer with a global drag-state bus, two React hooks for drag sources and drop targets, and integrations in FilesApp (file drag source) and MessagesApp (file drop target). Includes comprehensive test coverage and design documentation.

Changes

Cohort / File(s) Summary
DnD Core Infrastructure
desktop/src/shell/dnd/types.ts, desktop/src/shell/dnd/dnd-bus.ts, desktop/src/shell/dnd/use-drag-source.ts, desktop/src/shell/dnd/use-drop-target.ts
New discriminated union DragPayload type (file, message, knowledge, canvas-block). Singleton event-based drag bus with 30s stale timeout. useDragSource hook initializes drag state and mirrors HTML5 dataTransfer. useDropTarget hook validates accepted kinds, tracks nested enter/leave via counter, and invokes drop callback with bus payloads.
DnD Test Suites
desktop/src/shell/dnd/__tests__/dnd-bus.test.ts, desktop/src/shell/dnd/__tests__/use-drag-source.test.tsx, desktop/src/shell/dnd/__tests__/use-drop-target.test.tsx
New Vitest test files covering drag bus pub/sub and stale timer behavior, drag source hook payload/disabled/htmlMirror handling, and drop target hook validation/enter-leave counter/error handling.
App Integrations
desktop/src/apps/FilesApp.tsx, desktop/src/apps/MessagesApp.tsx
FilesApp: New FileRow component with drag source for files (computes per-location vfsPath, spreads dragHandlers onto <tr>). MessagesApp: Shell-level drop target with hybrid native/shell file handling, conditional onDrop dispatching, and dynamic drop-zone styling based on isOver/isValidTarget.
Documentation & Specs
docs/superpowers/plans/2026-04-20-shell-cross-app-dnd.md, docs/superpowers/specs/2026-04-20-shell-cross-app-dnd-design.md
Implementation plan and design specification detailing drag payload kinds, bus architecture, hook APIs, per-app integration points, error handling, and test strategy.
Build Configuration & Artifacts
desktop/tsconfig.tsbuildinfo, static/desktop/assets/*.js
TypeScript build info updated with new DnD module entries. Multiple app bundles (ActivityApp, AgentBrowsersApp, BrowserApp, CalendarApp, ChannelsApp, ClusterApp, ContactsApp, GitHubApp, ImageViewerApp, ImagesApp, LibraryApp, ModelsApp, RedditApp, SecretsApp, StoreApp, TasksApp, ProvidersApp, SettingsApp, MCPApp) update vendor-icons import paths. FilesApp and MessagesApp bundles regenerated; ImportApp bundle deduplicated; MemoryApp adjusted agent/pipeline UI logic; MobileSplitView function signature reordered.

Sequence Diagram

sequenceDiagram
    participant FileRow as FilesApp<br/>(FileRow)
    participant DragBus as DnD Bus
    participant MsgApp as MessagesApp
    participant API as API

    FileRow->>FileRow: onDragStart triggered
    FileRow->>DragBus: useDragSource calls startDrag(file_payload)
    DragBus->>DragBus: Sets current payload, clears stale timer
    DragBus->>DragBus: Emits change event (30s timeout active)
    
    MsgApp->>DragBus: useDropTarget subscribes via useSyncExternalStore
    DragBus->>MsgApp: getCurrent() returns file_payload
    MsgApp->>MsgApp: Computes isValidTarget=true (file in accept list)
    MsgApp->>MsgApp: Sets isOver=true on dragEnter
    
    MsgApp->>MsgApp: onDrop triggered
    MsgApp->>DragBus: Reads current payload again
    MsgApp->>API: attachmentFromPath(path) → creates AttachmentRecord
    API->>MsgApp: Returns attachment with source metadata
    MsgApp->>DragBus: Calls endDrag() to clear bus
    DragBus->>DragBus: Clears current, cancels stale timer, emits change
    MsgApp->>MsgApp: Updates UI, sets isOver=false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Dragging files cross the app with glee,
A rabbit's bus makes drop targets free,
From Files to Messages, payloads fly,
Thirty seconds to stale—then goodbye!
Hooks and events in harmony!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Shell: cross-app drag-drop primitive + Files → Messages wiring' directly and accurately summarizes the main changes: implementing a drag-drop system and integrating it between FilesApp and MessagesApp.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/shell-cross-app-dnd

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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

try {
const r = onDrop(payload);
if (r && typeof (r as Promise<void>).then === "function") {
(r as Promise<void>).catch((err) => console.warn("drop handler failed", err));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: endDrag() is not called after successful drop. This leaves the payload active in the bus until the 30s stale timer triggers, which may cause unexpected drop targets to become valid after a completed drag operation.

Call endDrag() after the onDrop handler completes successfully.

const { payload, disabled = false, htmlMirror } = opts;
return {
dragHandlers: {
draggable: !disabled,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Missing check for existing active drag before starting new drag. If multiple drag sources receive dragStart events concurrently, the last one will overwrite the current payload without clearing the previous stale timer.

Add endDrag() at the beginning of onDragStart to safely reset existing state.

const Icon = getFileIcon(f.name, f.is_dir);
const relPath = f.path || (currentPath ? `${currentPath}/${f.name}` : f.name);

let vfsPath: string | null = 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.

WARNING: When vfsPath is null, this passes an empty string as path in the drag payload (due to ?? ""). Empty paths will fail validation in drop targets.

The payload should never have an empty path - ensure vfsPath is always valid when dragEnabled is true.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Apr 20, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

All Changes Verified

FilesApp.tsx: FileRow component correctly implements drag source
MessagesApp.tsx: Dual drop target handling for both native OS files and internal shell drags
dnd-bus.ts: Bus implementation verified with proper stale timeout
use-drag-source.ts: Defensive stale state clearing implemented
use-drop-target.ts: Correctly calls endDrag() after successful drop
✅ All 15 DnD unit tests pass
✅ No regressions introduced
✅ Keyboard inset handling correctly restored

Files Reviewed (12 files)
  • desktop/src/apps/FilesApp.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/shell/dnd/types.ts
  • desktop/src/shell/dnd/dnd-bus.ts
  • desktop/src/shell/dnd/use-drag-source.ts
  • desktop/src/shell/dnd/use-drop-target.ts
  • desktop/src/shell/dnd/__tests__/dnd-bus.test.ts
  • desktop/src/shell/dnd/__tests__/use-drag-source.test.tsx
  • desktop/src/shell/dnd/__tests__/use-drop-target.test.tsx
  • docs/superpowers/plans/2026-04-20-shell-cross-app-dnd.md
  • desktop/tsconfig.tsbuildinfo

Reviewed by seed-2-0-pro-260328 · 257,929 tokens

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: 3

🧹 Nitpick comments (5)
static/desktop/assets/ImportApp-CB2pqwOR.js (1)

1-1: Drag hover state can flicker with nested children.

On Line 1 (dropzone JSX), onDragLeave={() => x(false)} is used without an enter-counter. With nested elements, moving within the dropzone can trigger false “leave” events and clear hover state prematurely.

Consider switching this dropzone to the new shell DnD primitive (useDropTarget) or adding an enter-counter locally to stabilize isOver.

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

In `@static/desktop/assets/ImportApp-CB2pqwOR.js` at line 1, The dropzone's hover
state uses onDragLeave={() => x(false)} which can flicker because nested
children fire leave events; replace this with an enter-count approach (or swap
to the shell DnD primitive useDropTarget). Add a ref like dragCounterRef in the
ImportApp component, implement onDragEnter to increment the counter and call
x(true) when counter>0, implement onDragLeave to decrement and only call
x(false) when the counter reaches 0, and keep the existing onDrop handler to
reset the counter and call x(false); update the JSX props on the dropzone
element (the component rendered as C with props onDragOver/onDragLeave/onDrop)
to use the new onDragEnter/onDragLeave logic and ensure the counter is reset in
z (onDrop) and any early exits.
desktop/src/apps/FilesApp.tsx (1)

279-285: Redundant key prop on inner <tr> element.

The key={f.path || f.name} on the <tr> is unnecessary since React only needs the key on the direct child of the .map() call (already provided on <FileRow> at line 1361). Remove to reduce noise.

♻️ Suggested fix
     return (
       <tr
-        key={f.path || f.name}
         data-file-row
         className="border-b border-white/5 hover:bg-shell-surface/50 transition-colors group"
         {...dragHandlers}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/FilesApp.tsx` around lines 279 - 285, The inner <tr> in
FilesApp.tsx has a redundant key prop (key={f.path || f.name}); remove that key
from the <tr> element so React uses the key already provided on the parent
FileRow component, leaving the dragHandlers, data-file-row attribute and
className intact—update the JSX in the render where <tr ...> is created (inside
FilesApp / FileRow render) to drop the key prop.
docs/superpowers/plans/2026-04-20-shell-cross-app-dnd.md (1)

95-110: Stale timer reset test has subtle timing edge case.

At Line 106-109, the test advances 15s after restart, then 20s more (35s total after restart). However, the stale timeout is 30s, so getCurrent() should be null at 30s, not 35s. The test assertion at Line 108 (expect(getCurrent()).not.toBeNull()) passes because 15s < 30s, and Line 109 (expect(getCurrent()).toBeNull()) passes because 35s > 30s.

The logic is correct, but the comment "35s after restart total" could mislead readers into thinking the timeout is 35s. Consider clarifying:

-    vi.advanceTimersByTime(20_000); // 35s after restart total
+    vi.advanceTimersByTime(20_000); // 35s after restart — well past 30s stale timeout
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-shell-cross-app-dnd.md` around lines 95 -
110, Update the misleading inline comment in the "starting a new drag resets the
stale timer" test: in the block using startDrag, getCurrent, and
vi.advanceTimersByTime, replace the comment "35s after restart total" with a
clarifying note like "35s after restart total (exceeds 30s stale timeout)" or
otherwise state that the second advance exceeds the 30s timeout so getCurrent()
should be null; keep the test logic unchanged.
static/desktop/assets/FilesApp-C4Wn3SGu.js (1)

1-1: Hardcoded MIME type may reduce drop target usefulness.

The drag payload uses mime_type:"application/octet-stream" regardless of actual file type. While the source FilesApp.tsx snippet shows mime_type: entry.mime_type ?? guessFromExt(entry.name) in the plan, the bundled code appears to hardcode it. This could limit drop targets that want to filter by specific MIME types (e.g., accepting only images).

If the intent is to support MIME-based filtering in the future, consider deriving the MIME type from the file extension using the existing is extension-to-icon mapping or a dedicated lookup.

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

In `@static/desktop/assets/FilesApp-C4Wn3SGu.js` at line 1, The drag payload
currently hardcodes mime_type:"application/octet-stream" (see the us(...) call
to ls(...) and ls(...)’s payload), which prevents MIME-based drop filtering;
update the payload to derive mime_type from the file entry (use entry.mime_type
if present, otherwise compute from the file extension), e.g. add a small helper
that maps extensions using the existing is mapping (or implement guessFromExt)
and use that value in the payload passed to ls (and/or inside ls when building
the payload) instead of the hardcoded "application/octet-stream".
static/desktop/assets/MessagesApp-BLfj1ISY.js (1)

1-1: Implement actual retry logic or hide the retry button for attachment upload failures.

The retry button on failed attachments misleads users—clicking it only updates the error message to "retry not yet supported — remove and re-add" rather than retrying the upload. Either implement actual retry functionality (re-running Me() or attachmentFromPath), or remove the "retry" button and show the limitation upfront in the error message instead.

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

In `@static/desktop/assets/MessagesApp-BLfj1ISY.js` at line 1, The retry button
for failed attachments currently only sets an error text instead of
re-uploading; implement real retry in the onRetry handler passed to Ts by
locating the failed attachment in the G state, set uploading=true and clear
error, then re-run the appropriate upload function (Me for disk-sourced
attachments, yt for workspace/agent-workspace sources) and update the G entry
with the returned record and uploading:false or set error on failure; reference
Ts (renders retry button), Me (uploads File), yt (uploads from workspace path),
the G state/ R updater where Ts is invoked, and ensure channel id x is forwarded
when calling Me.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 242-264: The shell drop handler (useDropTarget → onDrop /
shellFileDropTarget) currently processes dropped files even when the channel is
archived; add an early guard that checks isCurrentArchived (similar to other
attachment entry points) and returns immediately if true or if no
selectedChannel, so no pending attachment is added; ensure the same guard is
added to the native dataTransfer.files handling path so both drop flows skip
resolving paths and calling attachmentFromPath and setPendingAttachments when
isCurrentArchived is true.

In `@desktop/src/shell/dnd/use-drop-target.ts`:
- Around line 30-60: Update the drag handlers to only treat a drag as active
when there's a current payload and its kind is accepted: in onDragEnter and
onDragOver call getCurrent() and verify payload && accept.includes(payload.kind)
(and skip early if disabled) before calling e.preventDefault() or incrementing
enterCounter / setting setIsOver(true); keep onDragLeave and onDrop behavior for
resetting enterCounter/setIsOver but in onDrop also gate calling onDrop(payload)
behind the same payload && accept.includes check (reuse the existing
getCurrent() usage) so unrelated drags no longer trigger hover visuals or
preventDefault().

In `@static/desktop/assets/ImportApp-CB2pqwOR.js`:
- Line 1: The upload loop in async function T() always increments the success
counter and reports "Uploaded ..." even on fetch failures or non-2xx responses;
change T to only increment the success counter when a POST returns ok, track
failures (e.g., failedCount or failedNames) when fetch throws or response.ok is
false, and update the final status message (the o state setter) to report
successes vs failures (e.g., "Uploaded X of Y files" and list failed files or
count). Locate and update async function T, the loop over i, the FormData/
fetch("/api/import/upload") call, the v state updates (progress), and the final
o(...) call to reflect real success/failure counts and accurate progress math.
Ensure catch blocks and non-ok responses both mark failures and still advance
progress UI correctly.

---

Nitpick comments:
In `@desktop/src/apps/FilesApp.tsx`:
- Around line 279-285: The inner <tr> in FilesApp.tsx has a redundant key prop
(key={f.path || f.name}); remove that key from the <tr> element so React uses
the key already provided on the parent FileRow component, leaving the
dragHandlers, data-file-row attribute and className intact—update the JSX in the
render where <tr ...> is created (inside FilesApp / FileRow render) to drop the
key prop.

In `@docs/superpowers/plans/2026-04-20-shell-cross-app-dnd.md`:
- Around line 95-110: Update the misleading inline comment in the "starting a
new drag resets the stale timer" test: in the block using startDrag, getCurrent,
and vi.advanceTimersByTime, replace the comment "35s after restart total" with a
clarifying note like "35s after restart total (exceeds 30s stale timeout)" or
otherwise state that the second advance exceeds the 30s timeout so getCurrent()
should be null; keep the test logic unchanged.

In `@static/desktop/assets/FilesApp-C4Wn3SGu.js`:
- Line 1: The drag payload currently hardcodes
mime_type:"application/octet-stream" (see the us(...) call to ls(...) and
ls(...)’s payload), which prevents MIME-based drop filtering; update the payload
to derive mime_type from the file entry (use entry.mime_type if present,
otherwise compute from the file extension), e.g. add a small helper that maps
extensions using the existing is mapping (or implement guessFromExt) and use
that value in the payload passed to ls (and/or inside ls when building the
payload) instead of the hardcoded "application/octet-stream".

In `@static/desktop/assets/ImportApp-CB2pqwOR.js`:
- Line 1: The dropzone's hover state uses onDragLeave={() => x(false)} which can
flicker because nested children fire leave events; replace this with an
enter-count approach (or swap to the shell DnD primitive useDropTarget). Add a
ref like dragCounterRef in the ImportApp component, implement onDragEnter to
increment the counter and call x(true) when counter>0, implement onDragLeave to
decrement and only call x(false) when the counter reaches 0, and keep the
existing onDrop handler to reset the counter and call x(false); update the JSX
props on the dropzone element (the component rendered as C with props
onDragOver/onDragLeave/onDrop) to use the new onDragEnter/onDragLeave logic and
ensure the counter is reset in z (onDrop) and any early exits.

In `@static/desktop/assets/MessagesApp-BLfj1ISY.js`:
- Line 1: The retry button for failed attachments currently only sets an error
text instead of re-uploading; implement real retry in the onRetry handler passed
to Ts by locating the failed attachment in the G state, set uploading=true and
clear error, then re-run the appropriate upload function (Me for disk-sourced
attachments, yt for workspace/agent-workspace sources) and update the G entry
with the returned record and uploading:false or set error on failure; reference
Ts (renders retry button), Me (uploads File), yt (uploads from workspace path),
the G state/ R updater where Ts is invoked, and ensure channel id x is forwarded
when calling Me.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4b7c74c-ad33-4138-8c3f-67a6c365967b

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa6875 and 6c2fdc0.

📒 Files selected for processing (73)
  • desktop/src/apps/FilesApp.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/shell/dnd/__tests__/dnd-bus.test.ts
  • desktop/src/shell/dnd/__tests__/use-drag-source.test.tsx
  • desktop/src/shell/dnd/__tests__/use-drop-target.test.tsx
  • desktop/src/shell/dnd/dnd-bus.ts
  • desktop/src/shell/dnd/types.ts
  • desktop/src/shell/dnd/use-drag-source.ts
  • desktop/src/shell/dnd/use-drop-target.ts
  • desktop/tsconfig.tsbuildinfo
  • docs/superpowers/plans/2026-04-20-shell-cross-app-dnd.md
  • docs/superpowers/specs/2026-04-20-shell-cross-app-dnd-design.md
  • static/desktop/assets/ActivityApp-B9wIvJAK.js
  • static/desktop/assets/AgentBrowsersApp-Ch7Kx-O4.js
  • static/desktop/assets/AgentsApp-BZw6W6Vy.js
  • static/desktop/assets/BrowserApp-KbremVMB.js
  • static/desktop/assets/CalendarApp-DZD5-sDA.js
  • static/desktop/assets/ChannelsApp-BveF_Oog.js
  • static/desktop/assets/ClusterApp-oYzyi0Dp.js
  • static/desktop/assets/ContactsApp-DDczp9Hc.js
  • static/desktop/assets/FilesApp-Bm-rxwrE.js
  • static/desktop/assets/FilesApp-C4Wn3SGu.js
  • static/desktop/assets/GitHubApp-B8-CS8Px.js
  • static/desktop/assets/ImageViewerApp-IGHKuKLt.js
  • static/desktop/assets/ImagesApp-DrJsImHG.js
  • static/desktop/assets/ImportApp-CB2pqwOR.js
  • static/desktop/assets/ImportApp-DBAV17Xb.js
  • static/desktop/assets/LibraryApp-BD1Wtjm7.js
  • static/desktop/assets/MCPApp-COa1XWmj.js
  • static/desktop/assets/MemoryApp-Dw5harOM.js
  • static/desktop/assets/MessagesApp-BLfj1ISY.js
  • static/desktop/assets/MessagesApp-BkrxHSyC.js
  • static/desktop/assets/MobileSplitView-B-4MQO2C.js
  • static/desktop/assets/ModelsApp-BGQXnEI3.js
  • static/desktop/assets/ProvidersApp-CLIvGvDZ.js
  • static/desktop/assets/RedditApp-CVpnnpBP.js
  • static/desktop/assets/SecretsApp-Dq-GKn40.js
  • static/desktop/assets/SettingsApp-BSwNHIqf.js
  • static/desktop/assets/StoreApp-B9C6ujYf.js
  • static/desktop/assets/TasksApp-CcqxzxyH.js
  • static/desktop/assets/TextEditorApp-DX1ldFmW.js
  • static/desktop/assets/XApp-CKMiMFvk.js
  • static/desktop/assets/YouTubeApp-CAVU-1ZB.js
  • static/desktop/assets/chat-D-JXvZwU.js
  • static/desktop/assets/chat-D-WzE2QU.js
  • static/desktop/assets/dnd-bus-CdEoCWzR.js
  • static/desktop/assets/index-BEv-dcGk.js
  • static/desktop/assets/index-BlduOVkv.js
  • static/desktop/assets/index-BqnE0gGN.js
  • static/desktop/assets/index-Ce1hN6V-.js
  • static/desktop/assets/index-ClPK3Mg_.js
  • static/desktop/assets/index-CniRZ7Tm.js
  • static/desktop/assets/index-Crp4FTaY.js
  • static/desktop/assets/index-DGMKK4xB.js
  • static/desktop/assets/index-DMMUxG_R.js
  • static/desktop/assets/index-DTZBLne8.js
  • static/desktop/assets/index-DU8P0UT_.js
  • static/desktop/assets/index-DUWvMC11.js
  • static/desktop/assets/index-DuDKTnwy.js
  • static/desktop/assets/index-DwTu_mTz.js
  • static/desktop/assets/index-ESMkhiX0.js
  • static/desktop/assets/index-HCqzTdtf.js
  • static/desktop/assets/index-OlIDYpbO.js
  • static/desktop/assets/main-DJzY8MYL.js
  • static/desktop/assets/main-DncG9bdV.js
  • static/desktop/assets/tokens-BrwHLUZg.css
  • static/desktop/assets/tokens-C1AR9-o7.css
  • static/desktop/assets/tokens-C91S_gMx.js
  • static/desktop/assets/vendor-codemirror-aNcHammg.js
  • static/desktop/assets/vendor-icons--yQaiqiL.js
  • static/desktop/chat.html
  • static/desktop/index.html
  • tests/e2e/test_cross_app_dnd.py
💤 Files with no reviewable changes (3)
  • static/desktop/assets/ImportApp-DBAV17Xb.js
  • static/desktop/assets/FilesApp-Bm-rxwrE.js
  • static/desktop/assets/MessagesApp-BkrxHSyC.js

Comment thread desktop/src/apps/MessagesApp.tsx Outdated
Comment thread desktop/src/shell/dnd/use-drop-target.ts
@@ -0,0 +1 @@
import{r as l,j as t}from"./vendor-react-l6srOxy7.js";import{L as U,C,c as k,B as p}from"./toolbar-UW6q5pkx.js";import{ab as f,aj as B,y as M,an as O}from"./vendor-icons--yQaiqiL.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";const g=[".txt",".md",".pdf",".html",".json",".csv"],L=["text/plain","text/markdown","application/pdf","text/html","application/json","text/csv"];function R(c){return c<1024?`${c} B`:c<1024*1024?`${(c/1024).toFixed(1)} KB`:`${(c/(1024*1024)).toFixed(1)} MB`}function Y({windowId:c}){const[S,D]=l.useState([]),[r,E]=l.useState(""),[i,j]=l.useState([]),[A,x]=l.useState(!1),[h,b]=l.useState(!1),[u,v]=l.useState(0),[y,w]=l.useState(!1),[d,o]=l.useState(null),m=l.useRef(null);l.useEffect(()=>{(async()=>{try{const e=await fetch("/api/agents",{headers:{Accept:"application/json"}});if(e.ok&&(e.headers.get("content-type")??"").includes("application/json")){const a=await e.json();Array.isArray(a)&&a.length>0&&D(a.map(n=>String(n.name??"unknown")))}}catch{}})()},[]);const $=l.useCallback(e=>{var a;const s="."+((a=e.name.split(".").pop())==null?void 0:a.toLowerCase());return g.includes(s)||L.includes(e.type)},[]);function N(e){const a=e.filter($).map(n=>({id:`${n.name}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,file:n,name:n.name,size:n.size}));j(n=>[...n,...a]),o(null)}function z(e){e.preventDefault(),x(!1);const s=Array.from(e.dataTransfer.files);N(s)}function F(e){e.target.files&&N(Array.from(e.target.files)),e.target.value=""}function I(e){j(s=>s.filter(a=>a.id!==e))}async function T(){if(!r||i.length===0)return;b(!0),v(0),o(null);const e=i.length;let s=0;for(const a of i){const n=new FormData;n.append("file",a.file),n.append("agent",r);try{await fetch("/api/import/upload",{method:"POST",body:n})}catch{}s++,v(Math.round(s/e*100))}b(!1),o(`Uploaded ${e} file${e!==1?"s":""} for ${r}`)}async function P(){if(r){w(!0),o(null);try{(await fetch("/api/import/embed",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agent:r})})).ok?o("Embedding complete. Memory updated."):o("Embedding request sent. Check agent memory.")}catch{o("Could not reach embed endpoint. API may not be available.")}w(!1)}}return t.jsxs("div",{className:"flex flex-col h-full bg-shell-bg text-shell-text select-none",children:[t.jsxs("div",{className:"flex items-center gap-2 px-4 py-3 border-b border-white/5",children:[t.jsx(f,{size:18,className:"text-accent"}),t.jsx("h1",{className:"text-sm font-semibold",children:"Import"})]}),t.jsxs("div",{className:"flex-1 overflow-auto p-4 space-y-4",children:[t.jsxs("div",{className:"space-y-1.5",children:[t.jsx(U,{htmlFor:"import-agent",children:"Target Agent"}),t.jsxs("select",{id:"import-agent",value:r,onChange:e=>E(e.target.value),className:"flex h-9 w-full max-w-sm rounded-lg border border-white/10 bg-shell-bg-deep px-3 py-1 text-sm text-shell-text focus-visible:outline-none focus-visible:border-accent/40 focus-visible:ring-2 focus-visible:ring-accent/20",children:[t.jsx("option",{value:"",children:"Select an agent..."}),S.map(e=>t.jsx("option",{value:e,children:e},e))]})]}),t.jsx(C,{onDragOver:e=>{e.preventDefault(),x(!0)},onDragLeave:()=>x(!1),onDrop:z,className:`border-2 border-dashed transition-colors cursor-pointer ${A?"border-accent bg-accent/5":"border-white/10 hover:border-white/20"}`,onClick:()=>{var e;return(e=m.current)==null?void 0:e.click()},role:"button","aria-label":"Drop files here or click to browse",tabIndex:0,onKeyDown:e=>{var s;(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),(s=m.current)==null||s.click())},children:t.jsxs(k,{className:"flex flex-col items-center justify-center gap-3 p-8",children:[t.jsx(f,{size:32,className:"text-shell-text-tertiary"}),t.jsxs("div",{className:"text-center",children:[t.jsx("p",{className:"text-sm text-shell-text-secondary",children:"Drag and drop files here"}),t.jsx("p",{className:"text-xs text-shell-text-tertiary mt-1",children:g.join(", ")})]}),t.jsx(p,{variant:"secondary",size:"sm",onClick:e=>{var s;e.stopPropagation(),(s=m.current)==null||s.click()},children:"Browse"}),t.jsx("input",{ref:m,type:"file",multiple:!0,accept:g.join(","),onChange:F,className:"hidden","aria-label":"Select files to import"})]})}),i.length>0&&t.jsxs("div",{className:"space-y-1.5",children:[t.jsxs("h2",{className:"text-xs text-shell-text-secondary font-medium",children:["Queued Files (",i.length,")"]}),i.map(e=>t.jsx(C,{children:t.jsxs(k,{className:"flex items-center gap-3 px-3.5 py-2.5",children:[t.jsx(B,{size:14,className:"text-shell-text-tertiary shrink-0"}),t.jsx("span",{className:"text-sm flex-1 truncate",children:e.name}),t.jsx("span",{className:"text-xs text-shell-text-tertiary tabular-nums shrink-0",children:R(e.size)}),t.jsx(p,{variant:"ghost",size:"icon",onClick:()=>I(e.id),className:"h-7 w-7 hover:text-red-400 hover:bg-red-500/15","aria-label":`Remove ${e.name}`,children:t.jsx(M,{size:14})})]})},e.id))]}),h&&t.jsxs("div",{className:"space-y-1.5",children:[t.jsxs("div",{className:"flex items-center justify-between text-xs text-shell-text-secondary",children:[t.jsx("span",{children:"Uploading..."}),t.jsxs("span",{className:"tabular-nums",children:[u,"%"]})]}),t.jsx("div",{className:"h-2 w-full rounded-full bg-white/5",role:"progressbar","aria-valuenow":u,"aria-valuemin":0,"aria-valuemax":100,children:t.jsx("div",{className:"h-full rounded-full bg-accent transition-all",style:{width:`${u}%`}})})]}),d&&t.jsx("p",{className:`text-xs ${d.includes("complete")||d.includes("Uploaded")?"text-emerald-400":"text-amber-400"}`,children:d}),t.jsxs("div",{className:"flex gap-2",children:[t.jsxs(p,{onClick:T,disabled:!r||i.length===0||h,children:[t.jsx(f,{size:14}),h?"Uploading...":"Upload"]}),t.jsxs(p,{variant:"secondary",onClick:P,disabled:!r||y,className:"bg-violet-600 text-white hover:bg-violet-500",children:[t.jsx(O,{size:14}),y?"Embedding...":"Embed"]})]})]})]})}export{Y as ImportApp};
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

Upload result reporting is incorrect on partial/total failures.

On Line 1 (async function T()), each iteration increments progress and the final status always says Uploaded ... even when fetch fails or returns non-2xx. This misreports outcome to users.

Proposed fix (apply in source desktop/src/apps/ImportApp.tsx, then rebuild asset)
 async function uploadAll() {
   if (!selectedAgent || queuedFiles.length === 0) return;
   setUploading(true);
   setProgress(0);
   setStatus(null);

   const total = queuedFiles.length;
-  let done = 0;
+  let done = 0;
+  let success = 0;
+  const failed: string[] = [];

   for (const item of queuedFiles) {
     const form = new FormData();
     form.append("file", item.file);
     form.append("agent", selectedAgent);

     try {
-      await fetch("/api/import/upload", { method: "POST", body: form });
+      const res = await fetch("/api/import/upload", { method: "POST", body: form });
+      if (res.ok) success++;
+      else failed.push(item.name);
     } catch {
+      failed.push(item.name);
     }

     done++;
     setProgress(Math.round((done / total) * 100));
   }

   setUploading(false);
-  setStatus(`Uploaded ${total} file${total !== 1 ? "s" : ""} for ${selectedAgent}`);
+  if (failed.length === 0) {
+    setStatus(`Uploaded ${success} file${success !== 1 ? "s" : ""} for ${selectedAgent}`);
+  } else {
+    setStatus(
+      `Uploaded ${success}/${total} file${total !== 1 ? "s" : ""}. Failed: ${failed.join(", ")}`
+    );
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/ImportApp-CB2pqwOR.js` at line 1, The upload loop in
async function T() always increments the success counter and reports "Uploaded
..." even on fetch failures or non-2xx responses; change T to only increment the
success counter when a POST returns ok, track failures (e.g., failedCount or
failedNames) when fetch throws or response.ok is false, and update the final
status message (the o state setter) to report successes vs failures (e.g.,
"Uploaded X of Y files" and list failed files or count). Locate and update async
function T, the loop over i, the FormData/ fetch("/api/import/upload") call, the
v state updates (progress), and the final o(...) call to reflect real
success/failure counts and accurate progress math. Ensure catch blocks and
non-ok responses both mark failures and still advance progress UI correctly.

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: 1

♻️ Duplicate comments (2)
desktop/src/apps/MessagesApp.tsx (2)

242-269: ⚠️ Potential issue | 🟠 Major

Disable the shell drop target when the chat is archived.

onDrop bails out for archived channels, but the target itself is still active, so archived chats still light up as valid drop zones and intercept the drag. Compute the archived state before this hook and pass it through disabled so the hover state and handler gating stay consistent with the rest of the composer.

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

In `@desktop/src/apps/MessagesApp.tsx` around lines 242 - 269, Compute whether the
selected channel is archived before calling useDropTarget (e.g. derive a boolean
like isSelectedChannelArchived by finding selectedChannel in allChannels and
checking c.settings?.archived) and pass that boolean into useDropTarget via its
disabled prop so shellFileDropTarget is disabled for archived chats; keep the
existing onDrop guard but ensure the hook receives
disabled={isSelectedChannelArchived} to prevent hover/highlight and interception
for archived channels.

1340-1351: ⚠️ Potential issue | 🟠 Major

Native file drops still bypass the archived-channel guard.

This branch uploads dataTransfer.files even when the selected chat is archived, so users can still end up with unsendable pending attachment chips. Mirror the same early return here before enqueueing uploads.

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

In `@desktop/src/apps/MessagesApp.tsx` around lines 1340 - 1351, The onDrop
handler branch that processes e.dataTransfer.files currently enqueues uploads
and pending attachments without checking if the selectedChannel is archived; add
the same archived-channel guard used elsewhere (check selectedChannel?.archived
or call the existing isArchived utility) at the start of this block and return
early before calling setPendingAttachments or uploadDiskFile when the channel is
archived, so setPendingAttachments, uploadDiskFile, and the Math.random id
generation are skipped for archived channels (refer to the onDrop handler,
selectedChannel, setPendingAttachments, and uploadDiskFile symbols to locate the
code).
🧹 Nitpick comments (1)
static/desktop/assets/MCPApp-Dp9wLdka.js (1)

1-2: Handle clipboard write failures in the “Copy all logs” action.

The current handler assumes navigator.clipboard.writeText(...) always succeeds; on rejection, users get a silent failure. Wrap this path in try/catch and only show the copied state on success.

Proposed patch
-async function j(){await navigator.clipboard.writeText(n.join(`
-
-`)),u(!0),setTimeout(()=>u(!1),1500)}
+async function j(){
+  try{
+    await navigator.clipboard.writeText(n.join("\n"));
+    u(!0);
+    setTimeout(()=>u(!1),1500);
+  }catch{
+    u(!1);
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/MCPApp-Dp9wLdka.js` around lines 1 - 2, The "Copy all
logs" handler in Se currently assumes
navigator.clipboard.writeText(n.join(`\n`)) always succeeds and sets the copied
flag (u) unconditionally; wrap the clipboard write in a try/catch inside the j
function in Se, only set the copied state (u(!0)) and start the timeout when
writeText resolves, and handle rejection by leaving u false and optionally
setting an error state or logging (do not change other UI flags). Locate Se ->
function j and update its implementation to await
navigator.clipboard.writeText(...) inside try/catch so failures no longer
silently show the "copied" state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src/apps/FilesApp.tsx`:
- Around line 246-280: The grid view doesn't get the drag source because
useDragSource and its dragHandlers are only used inside FileRow (list view);
update FilesApp so the same drag setup is used for grid cards: extract the
vfsPath/dragEnabled logic and the useDragSource call (payload, disabled,
htmlMirror) from FileRow into a reusable spot (or compute them in the parent
where grid/list rendering happens), then attach the resulting dragHandlers to
the grid card wrapper element (the component rendering file cards in grid view)
the same way FileRow attaches them. Ensure you reuse the same symbols: compute
vfsPath (workspace or agent via agentSlug/isAgentLocation), set dragEnabled =
!!vfsPath && !f.is_dir, call useDragSource with the same payload fields (kind,
path, mime_type, size, name) and pass dragHandlers to the grid card so grid view
supports the identical drag behavior.

---

Duplicate comments:
In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 242-269: Compute whether the selected channel is archived before
calling useDropTarget (e.g. derive a boolean like isSelectedChannelArchived by
finding selectedChannel in allChannels and checking c.settings?.archived) and
pass that boolean into useDropTarget via its disabled prop so
shellFileDropTarget is disabled for archived chats; keep the existing onDrop
guard but ensure the hook receives disabled={isSelectedChannelArchived} to
prevent hover/highlight and interception for archived channels.
- Around line 1340-1351: The onDrop handler branch that processes
e.dataTransfer.files currently enqueues uploads and pending attachments without
checking if the selectedChannel is archived; add the same archived-channel guard
used elsewhere (check selectedChannel?.archived or call the existing isArchived
utility) at the start of this block and return early before calling
setPendingAttachments or uploadDiskFile when the channel is archived, so
setPendingAttachments, uploadDiskFile, and the Math.random id generation are
skipped for archived channels (refer to the onDrop handler, selectedChannel,
setPendingAttachments, and uploadDiskFile symbols to locate the code).

---

Nitpick comments:
In `@static/desktop/assets/MCPApp-Dp9wLdka.js`:
- Around line 1-2: The "Copy all logs" handler in Se currently assumes
navigator.clipboard.writeText(n.join(`\n`)) always succeeds and sets the copied
flag (u) unconditionally; wrap the clipboard write in a try/catch inside the j
function in Se, only set the copied state (u(!0)) and start the timeout when
writeText resolves, and handle rejection by leaving u false and optionally
setting an error state or logging (do not change other UI flags). Locate Se ->
function j and update its implementation to await
navigator.clipboard.writeText(...) inside try/catch so failures no longer
silently show the "copied" state.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6052285d-6028-43d8-ac3e-c57b2cf6d28b

📥 Commits

Reviewing files that changed from the base of the PR and between 6c2fdc0 and 40adaac.

📒 Files selected for processing (14)
  • desktop/src/apps/FilesApp.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/shell/dnd/__tests__/use-drop-target.test.tsx
  • desktop/src/shell/dnd/use-drag-source.ts
  • desktop/src/shell/dnd/use-drop-target.ts
  • static/desktop/assets/FilesApp-DpnvIlme.js
  • static/desktop/assets/MCPApp-Dp9wLdka.js
  • static/desktop/assets/MessagesApp-C-Qp-BQP.js
  • static/desktop/assets/ProvidersApp-f_IA1pBY.js
  • static/desktop/assets/SettingsApp-53U4rH_g.js
  • static/desktop/assets/chat-6DvfRGaD.js
  • static/desktop/assets/main-tvByLW-C.js
  • static/desktop/chat.html
  • static/desktop/index.html
✅ Files skipped from review due to trivial changes (2)
  • static/desktop/assets/ProvidersApp-f_IA1pBY.js
  • desktop/src/shell/dnd/tests/use-drop-target.test.tsx

Comment on lines +246 to +280
function FileRow({
f,
location,
currentPath,
navigateTo,
isWritable,
deleteConfirm,
handleDelete,
setDeleteConfirm,
}: FileRowProps) {
const Icon = getFileIcon(f.name, f.is_dir);
const relPath = f.path || (currentPath ? `${currentPath}/${f.name}` : f.name);

let vfsPath: string | null = null;
if (location === "workspace") {
vfsPath = `/workspaces/user/${relPath}`;
} else if (isAgentLocation(location)) {
vfsPath = `/workspaces/agent/${agentSlug(location)}/${relPath}`;
}

const dragEnabled = !!vfsPath && !f.is_dir;
const { dragHandlers } = useDragSource({
// When dragEnabled is false, `disabled: true` short-circuits the payload
// before it ever lands on the bus — the empty-string placeholder below
// is never read.
payload: {
kind: "file",
path: vfsPath ?? "",
mime_type: "application/octet-stream",
size: f.size ?? 0,
name: f.name,
},
disabled: !dragEnabled,
htmlMirror: dragEnabled && vfsPath ? { "text/plain": vfsPath } : undefined,
});
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

Wire the drag source into grid view too.

useDragSource() only gets applied through FileRow, and FileRow is only rendered in list view. Since FilesApp still defaults to grid view, the new Files → Messages flow is unavailable in the default layout until users manually switch views. Reuse the same drag config on the grid cards as well.

Also applies to: 1362-1374

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

In `@desktop/src/apps/FilesApp.tsx` around lines 246 - 280, The grid view doesn't
get the drag source because useDragSource and its dragHandlers are only used
inside FileRow (list view); update FilesApp so the same drag setup is used for
grid cards: extract the vfsPath/dragEnabled logic and the useDragSource call
(payload, disabled, htmlMirror) from FileRow into a reusable spot (or compute
them in the parent where grid/list rendering happens), then attach the resulting
dragHandlers to the grid card wrapper element (the component rendering file
cards in grid view) the same way FileRow attaches them. Ensure you reuse the
same symbols: compute vfsPath (workspace or agent via
agentSlug/isAgentLocation), set dragEnabled = !!vfsPath && !f.is_dir, call
useDragSource with the same payload fields (kind, path, mime_type, size, name)
and pass dragHandlers to the grid card so grid view supports the identical drag
behavior.

@jaylfc jaylfc force-pushed the feat/shell-cross-app-dnd branch from 40adaac to 0489c00 Compare April 20, 2026 12:12
- shell/dnd/ — types, bus, useDragSource, useDropTarget (15 tests)
- FilesApp: file rows become drag sources with VFS path payload
- MessagesApp: message list is a drop target that converts VFS paths
  into chat attachments via existing attachmentFromPath; blocked in
  archived channels; gated on isValidTarget so unrelated drags aren't
  intercepted; OS-level file drops retain existing native path
- endDrag() called after successful drop so next target doesn't stay valid
- E2E stub: drag file from Files into Messages composer
@jaylfc jaylfc force-pushed the feat/shell-cross-app-dnd branch from 0489c00 to 7873947 Compare April 20, 2026 12:14
@jaylfc jaylfc merged commit 8ad0d8d into master Apr 20, 2026
8 checks passed
@jaylfc jaylfc deleted the feat/shell-cross-app-dnd branch April 25, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant