feat: stream list view and tasks #279

Merged
talksik merged 7 commits from ecosystem-prototype into main 2026-06-12 19:26:50 +00:00
talksik commented 2026-06-12 17:21:08 +00:00 (Migrated from github.com)
  • first attempt at stream sidebar, tasks, and events
  • fix folder from root
  • cleanup folders and events, and condense changes

Summary by CodeRabbit

  • New Features

    • Full task support: create, compose, edit, assign, checklist, and mark done; task-specific views and previews.
    • Stream list mode and immersive playback toggle; new bottom controls and sidebar list view.
    • Container browsing: unified folder/root container view and create-folder/stream flow.
    • Lightweight textarea component and improved live-edit helpers.
  • Bug Fixes

    • Safer handling of unknown or malformed particle types and improved stream open/closed handling.
- **first attempt at stream sidebar, tasks, and events** - **fix folder from root** - **cleanup folders and events, and condense changes** <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Full task support: create, compose, edit, assign, checklist, and mark done; task-specific views and previews. * Stream list mode and immersive playback toggle; new bottom controls and sidebar list view. * Container browsing: unified folder/root container view and create-folder/stream flow. * Lightweight textarea component and improved live-edit helpers. * **Bug Fixes** * Safer handling of unknown or malformed particle types and improved stream open/closed handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
coderabbitai[bot] commented 2026-06-12 17:21:18 +00:00 (Migrated from github.com)

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 175d6b1a-aa0f-402e-bede-ae2549a93921

📥 Commits

Reviewing files that changed from the base of the PR and between 224b64f86b and 044f1a7457.

📒 Files selected for processing (7)
  • js/desktop/src/features/particles/delete-particle-overlay.tsx
  • js/desktop/src/features/particles/rename-stream-overlay.tsx
  • js/desktop/src/features/particles/stream-list-sidebar.tsx
  • js/desktop/src/features/particles/stream-members-overlay.tsx
  • js/desktop/src/features/particles/stream-top-bar.tsx
  • js/desktop/src/features/particles/stream-view.tsx
  • js/desktop/src/features/particles/task-particle-view.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • js/desktop/src/features/particles/task-particle-view.tsx
  • js/desktop/src/features/particles/stream-list-sidebar.tsx

📝 Walkthrough

Walkthrough

Adds task particle types and UI, generalizes container/stream browsing (player/list modes), integrates task compose and task views, centralizes display helpers, refactors list navigation/playback hooks, and hardens Firestore parsing on desktop and mobile.

Changes

Task, Container, and Stream Flow Updates

Layer / File(s) Summary
Task particle and unknown particle schemas
js/desktop/src/api/types.ts
Adds ChecklistItemSchema, TaskPropertiesSchema, and UnknownParticleSchema with inferred types. Updates ParticlePropertiesMap to swap quest for task. Extends stream variant with optional `status: 'open'
Compose intent store supports task
js/desktop/src/stores/compose-intent-store.ts
Extends ComposeIntent type with new 'task' variant and documents it in the intent-step table.
Reusable textarea control
js/desktop/src/components/ui/textarea.tsx
Adds Textarea component that wraps native textarea with Tailwind styling, merges className, and forwards props.
Task compose form component
js/desktop/src/features/compose/task-compose-step.tsx
Introduces TaskComposeStep with local state for title/notes/assignedTo, derives assignees from network, shapes TaskProperties payload with conditional fields, and supports keyboard submit/cancel.
Compose overlay task flow wiring
js/desktop/src/features/compose/compose-overlay.tsx
Extends ComposeStep union with task/configuring/submitting. Adds pendingArtifactRef for task storage, handleTaskIntent/handleArtifactSubmit handlers, task keyboard shortcut (D), and renders TaskComposeStep. Removes disabled prop gating and uses ConfigureContainerStep.
Generalized container configuration UI
js/desktop/src/features/compose/configure-container-step.tsx
Renames ConfigureStreamStep to ConfigureContainerStep, adds `kind?: 'stream'
Reusable fixed dwell timing hook
js/desktop/src/hooks/use-fixed-dwell.ts
Adds useFixedDwell for elapsed/progress tracking with id resets, pause support, and optional onProgress/onEnded callbacks.
Live collaborative draft field hook
js/desktop/src/hooks/use-live-draft-field.ts
Introduces useLiveDraftField for debounced write-through drafting, focus-aware remote value sync, and flush-on-blur/unmount behavior.
Task particle reading and editing view
js/desktop/src/features/particles/task-particle-view.tsx
Adds TaskParticleView with live draft editing for title/notes, whole-array checklist writes, assignee updates via set()/deleteField(), dwell integration, and checklist/assignee UI subcomponents.
Text particle uses fixed dwell hook
js/desktop/src/features/particles/text-particle-view.tsx
Refactors TextParticleView to delegate dwell/progress/ending to useFixedDwell instead of local interval management.
Shared particle display helper module
js/desktop/src/lib/particle-display.ts
Adds exported helpers: getParticleTypeIcon, getMessagePreview (with media/transcript handling), and getParticleDisplayName (with text truncation).
Task preview and unknown fallback updates
js/desktop/src/features/particles/particle-preview.tsx, js/desktop/src/features/particles/fallback-particle-view.tsx
Replaces QuestPreview with TaskPreview (title + done state styling). Updates fallback to show particle.raw_type for unknowns and fixed "Not supported" message.
Container children query hook
js/desktop/src/hooks/use-container-children.ts
Adds useContainerChildren to query container (stream/folder) and leaf particle children with optional stream-status filtering, visibility scopes, merge/sort by activity, and pagination.
Create particle hook path/visibility options
js/desktop/src/hooks/use-create-particle.ts
Extends CreateParticleParams with optional visibleTo and CreateStreamParticleParams with optional parentPath, enabling nested streams and visibility control.
Generalized list keyboard navigation hook
js/desktop/src/hooks/use-list-keyboard-nav.ts
Replaces useStreamKeyboardNav with useListKeyboardNav accepting generic items array and onOpen callback for item activation.
Container view with mixed children and creation modes
js/desktop/src/features/particles/container-view.tsx
Introduces ContainerView rendering status-filtered children list, keyboard shortcuts (N for stream/folder creation), folder/stream creation via ConfigureContainerStep, and ContainerControls subcomponent for hints/actions.
Particle children list refactor
js/desktop/src/features/particles/particle-children-list.tsx
Replaces ParticleListView with ParticleChildrenList supporting mixed particle types, onOpen callback, empty state, per-type row rendering (StreamRow/FolderRow/LeafRow), and keyboard shortcuts.
Folder and network root wrappers use container view
js/desktop/src/features/particles/folder-view.tsx, js/desktop/src/features/network-root.tsx
Simplifies FolderView and NetworkRoot to delegate rendering to ContainerView with narrowed/resolved paths.
Resolver layout and leaf routing changes
js/desktop/src/features/particles/particle-view-resolver.tsx
Wraps resolver states in Layout. Introduces LeafParticleView to detect deleted particles and route leaf types (task/media/text/paper) to appropriate viewers.
Stream view mode decision hook
js/desktop/src/hooks/use-stream-view-mode.ts
Adds useStreamViewMode to compute initial mode ('player' or 'list') from playback markers vs. stream activity and expose toggle() callback.
Playback/navigation key hook extensions
js/desktop/src/hooks/use-playback-keys.ts, js/desktop/src/hooks/use-stream-navigation-keys.ts
Extends usePlaybackKeys to expose spacePaused and resume(). Adds onToggleViewMode callback and L key handling to useStreamNavigationKeys.
Stream playback auto-advance option
js/desktop/src/hooks/use-stream-playback.ts
Adds optional autoAdvanceOnNew option (default true) to control whether newly created particles auto-advance playback.
Stream sidebar and bottom control components
js/desktop/src/features/particles/stream-list-sidebar.tsx, js/desktop/src/features/particles/stream-bottom-bar.tsx
Adds StreamListSidebar showing scrollable particle list with chat-row selection, BottomBar with playback indicator and exit countdown, and StreamViewControls with compose intent shortcuts (D for task, etc.).
Stream view player/list mode orchestration
js/desktop/src/features/particles/stream-view.tsx
Refactors StreamView to support player/list modes, mode-dependent auto-advance/chaining, conditional exit countdown, sidebar rendering, paused resume button, and keyboard mode toggle (L).
Desktop Firestore parsing resilience and activity updates
js/desktop/src/lib/firestore-particles.ts
Adds safeData wrapper, timestamp coercion for leaf properties, unknown-type fallback decoding, resilient read/subscription paths filtering nulls, and bumpParentLastChildCreatedAt to maintain activity ordering on creates.
Mobile Firestore safe parsing updates
js/mobile/src/lib/firestore-particles.ts
Introduces safeData helper and applies it to all particle read/subscription paths to filter out unparseable documents.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through task-filled streams,
it jots down titles, notes, and dreams—
live drafts hum and checklists grow,
safe snapshots guard the ebb and flow.
🐰 Tasks, lists, and modes in tow.

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 ecosystem-prototype
<!-- This is an auto-generated comment: summarize by coderabbit.ai --> <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/flowy-live/llink/pull/279?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> No actionable comments were generated in the recent review. 🎉 <details> <summary>ℹ️ Recent review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `175d6b1a-aa0f-402e-bede-ae2549a93921` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 224b64f86b013496a80f3ef8e0f420cf34d816fc and 044f1a7457b08146dc4068982bbea6983ec73a53. </details> <details> <summary>📒 Files selected for processing (7)</summary> * `js/desktop/src/features/particles/delete-particle-overlay.tsx` * `js/desktop/src/features/particles/rename-stream-overlay.tsx` * `js/desktop/src/features/particles/stream-list-sidebar.tsx` * `js/desktop/src/features/particles/stream-members-overlay.tsx` * `js/desktop/src/features/particles/stream-top-bar.tsx` * `js/desktop/src/features/particles/stream-view.tsx` * `js/desktop/src/features/particles/task-particle-view.tsx` </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary> * js/desktop/src/features/particles/task-particle-view.tsx * js/desktop/src/features/particles/stream-list-sidebar.tsx </details> </details> --- <!-- walkthrough_start --> <details> <summary>📝 Walkthrough</summary> ## Walkthrough Adds task particle types and UI, generalizes container/stream browsing (player/list modes), integrates task compose and task views, centralizes display helpers, refactors list navigation/playback hooks, and hardens Firestore parsing on desktop and mobile. ## Changes **Task, Container, and Stream Flow Updates** |Layer / File(s)|Summary| |---|---| |**Task particle and unknown particle schemas** <br> `js/desktop/src/api/types.ts`|Adds `ChecklistItemSchema`, `TaskPropertiesSchema`, and `UnknownParticleSchema` with inferred types. Updates `ParticlePropertiesMap` to swap `quest` for `task`. Extends `stream` variant with optional `status: 'open' | 'closed'`. Exports `isStreamOpen` helper and derives `ParticleType`/`CONTAINER_TYPES` from updated unions.| |**Compose intent store supports task** <br> `js/desktop/src/stores/compose-intent-store.ts`|Extends `ComposeIntent` type with new `'task'` variant and documents it in the intent-step table.| |**Reusable textarea control** <br> `js/desktop/src/components/ui/textarea.tsx`|Adds `Textarea` component that wraps native textarea with Tailwind styling, merges className, and forwards props.| |**Task compose form component** <br> `js/desktop/src/features/compose/task-compose-step.tsx`|Introduces `TaskComposeStep` with local state for title/notes/assignedTo, derives assignees from network, shapes `TaskProperties` payload with conditional fields, and supports keyboard submit/cancel.| |**Compose overlay task flow wiring** <br> `js/desktop/src/features/compose/compose-overlay.tsx`|Extends `ComposeStep` union with `task`/`configuring`/`submitting`. Adds `pendingArtifactRef` for task storage, `handleTaskIntent`/`handleArtifactSubmit` handlers, task keyboard shortcut (`D`), and renders `TaskComposeStep`. Removes `disabled` prop gating and uses `ConfigureContainerStep`.| |**Generalized container configuration UI** <br> `js/desktop/src/features/compose/configure-container-step.tsx`|Renames `ConfigureStreamStep` to `ConfigureContainerStep`, adds `kind?: 'stream' | 'folder'` prop (default `'stream'`), and makes name label/hints dynamic based on kind.| |**Reusable fixed dwell timing hook** <br> `js/desktop/src/hooks/use-fixed-dwell.ts`|Adds `useFixedDwell` for elapsed/progress tracking with id resets, pause support, and optional `onProgress`/`onEnded` callbacks.| |**Live collaborative draft field hook** <br> `js/desktop/src/hooks/use-live-draft-field.ts`|Introduces `useLiveDraftField` for debounced write-through drafting, focus-aware remote value sync, and flush-on-blur/unmount behavior.| |**Task particle reading and editing view** <br> `js/desktop/src/features/particles/task-particle-view.tsx`|Adds `TaskParticleView` with live draft editing for title/notes, whole-array checklist writes, assignee updates via `set()`/`deleteField()`, dwell integration, and checklist/assignee UI subcomponents.| |**Text particle uses fixed dwell hook** <br> `js/desktop/src/features/particles/text-particle-view.tsx`|Refactors `TextParticleView` to delegate dwell/progress/ending to `useFixedDwell` instead of local interval management.| |**Shared particle display helper module** <br> `js/desktop/src/lib/particle-display.ts`|Adds exported helpers: `getParticleTypeIcon`, `getMessagePreview` (with media/transcript handling), and `getParticleDisplayName` (with text truncation).| |**Task preview and unknown fallback updates** <br> `js/desktop/src/features/particles/particle-preview.tsx`, `js/desktop/src/features/particles/fallback-particle-view.tsx`|Replaces `QuestPreview` with `TaskPreview` (title + done state styling). Updates fallback to show `particle.raw_type` for unknowns and fixed "Not supported" message.| |**Container children query hook** <br> `js/desktop/src/hooks/use-container-children.ts`|Adds `useContainerChildren` to query container (stream/folder) and leaf particle children with optional stream-status filtering, visibility scopes, merge/sort by activity, and pagination.| |**Create particle hook path/visibility options** <br> `js/desktop/src/hooks/use-create-particle.ts`|Extends `CreateParticleParams` with optional `visibleTo` and `CreateStreamParticleParams` with optional `parentPath`, enabling nested streams and visibility control.| |**Generalized list keyboard navigation hook** <br> `js/desktop/src/hooks/use-list-keyboard-nav.ts`|Replaces `useStreamKeyboardNav` with `useListKeyboardNav` accepting generic `items` array and `onOpen` callback for item activation.| |**Container view with mixed children and creation modes** <br> `js/desktop/src/features/particles/container-view.tsx`|Introduces `ContainerView` rendering status-filtered children list, keyboard shortcuts (`N` for stream/folder creation), folder/stream creation via `ConfigureContainerStep`, and `ContainerControls` subcomponent for hints/actions.| |**Particle children list refactor** <br> `js/desktop/src/features/particles/particle-children-list.tsx`|Replaces `ParticleListView` with `ParticleChildrenList` supporting mixed particle types, `onOpen` callback, empty state, per-type row rendering (`StreamRow`/`FolderRow`/`LeafRow`), and keyboard shortcuts.| |**Folder and network root wrappers use container view** <br> `js/desktop/src/features/particles/folder-view.tsx`, `js/desktop/src/features/network-root.tsx`|Simplifies `FolderView` and `NetworkRoot` to delegate rendering to `ContainerView` with narrowed/resolved paths.| |**Resolver layout and leaf routing changes** <br> `js/desktop/src/features/particles/particle-view-resolver.tsx`|Wraps resolver states in `Layout`. Introduces `LeafParticleView` to detect deleted particles and route leaf types (task/media/text/paper) to appropriate viewers.| |**Stream view mode decision hook** <br> `js/desktop/src/hooks/use-stream-view-mode.ts`|Adds `useStreamViewMode` to compute initial `mode` (`'player'` or `'list'`) from playback markers vs. stream activity and expose `toggle()` callback.| |**Playback/navigation key hook extensions** <br> `js/desktop/src/hooks/use-playback-keys.ts`, `js/desktop/src/hooks/use-stream-navigation-keys.ts`|Extends `usePlaybackKeys` to expose `spacePaused` and `resume()`. Adds `onToggleViewMode` callback and `L` key handling to `useStreamNavigationKeys`.| |**Stream playback auto-advance option** <br> `js/desktop/src/hooks/use-stream-playback.ts`|Adds optional `autoAdvanceOnNew` option (default `true`) to control whether newly created particles auto-advance playback.| |**Stream sidebar and bottom control components** <br> `js/desktop/src/features/particles/stream-list-sidebar.tsx`, `js/desktop/src/features/particles/stream-bottom-bar.tsx`|Adds `StreamListSidebar` showing scrollable particle list with chat-row selection, `BottomBar` with playback indicator and exit countdown, and `StreamViewControls` with compose intent shortcuts (`D` for task, etc.).| |**Stream view player/list mode orchestration** <br> `js/desktop/src/features/particles/stream-view.tsx`|Refactors `StreamView` to support `player`/`list` modes, mode-dependent auto-advance/chaining, conditional exit countdown, sidebar rendering, paused resume button, and keyboard mode toggle (`L`).| |**Desktop Firestore parsing resilience and activity updates** <br> `js/desktop/src/lib/firestore-particles.ts`|Adds `safeData` wrapper, timestamp coercion for leaf properties, unknown-type fallback decoding, resilient read/subscription paths filtering nulls, and `bumpParentLastChildCreatedAt` to maintain activity ordering on creates.| |**Mobile Firestore safe parsing updates** <br> `js/mobile/src/lib/firestore-particles.ts`|Introduces `safeData` helper and applies it to all particle read/subscription paths to filter out unparseable documents.| ## Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes ## Poem > A rabbit hops through task-filled streams, > it jots down titles, notes, and dreams— > live drafts hum and checklists grow, > safe snapshots guard the ebb and flow. > 🐰✨ Tasks, lists, and modes in tow. </details> <!-- walkthrough_end --> <!-- finishing_touch_checkbox_start --> <details> <summary>✨ Finishing Touches</summary> <details> <summary>📝 Generate docstrings</summary> - [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR - [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch </details> <details> <summary>🧪 Generate unit tests (beta)</summary> - [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests - [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `ecosystem-prototype` </details> </details> <!-- finishing_touch_checkbox_end --> <!-- internal state start --> <!-- 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 --> <!-- internal state end -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-12 18:14:20 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
js/desktop/src/features/compose/compose-overlay.tsx (1)

523-535: ⚠️ Potential issue | 🟡 Minor | Quick win

Handle store-driven cancel actions in the task step.

handleCancelIntent() still skips 'task', so external cancel requests routed through the compose-intent subscription no-op while the task form is open. That makes the new task flow inconsistent with typing/configuring: local Esc works, but the shared store-driven cancel path does not.

Proposed fix
   const handleCancelIntent = useCallback(() => {
     const s = stepRef.current;
     if (s === 'recording' || s === 'reviewing') {
       if (recordingSourceRef.current === 'screen') {
         cancelScreenRecording();
       } else {
         cancelRecording();
       }
       cancel();
-    } else if (s === 'typing' || s === 'configuring' || s === 'picking') {
+    } else if (
+      s === 'typing' ||
+      s === 'task' ||
+      s === 'configuring' ||
+      s === 'picking'
+    ) {
       cancel();
     }
   }, [cancel, cancelRecording, cancelScreenRecording]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/compose/compose-overlay.tsx` around lines 523 - 535,
handleCancelIntent currently ignores the 'task' step so store-driven cancel
events routed via the compose-intent subscription are no-ops while the task form
is open; update the conditional in handleCancelIntent (the function that reads
stepRef.current) to treat 'task' the same as 'typing'/'configuring'/'picking'
(i.e., call cancel() for 'task'), ensuring both local Esc and shared store
cancel paths close the task form; adjust any related logic in the recording
branch if needed and keep the useCallback dependencies (cancel, cancelRecording,
cancelScreenRecording) intact.
🧹 Nitpick comments (4)
js/desktop/src/lib/firestore-particles.ts (2)

158-167: Quick win

Consider using console.error for unparseable particles.

While the defensive pattern is excellent for resilience, unparseable particles indicate data corruption or schema drift that should surface prominently. Using console.error instead of console.warn ensures these issues are visible in error tracking and monitoring systems.

📊 Proposed change
-    console.warn(`Skipping unparseable particle at ${snap.ref.path}:`, err);
+    console.error(`Skipping unparseable particle at ${snap.ref.path}:`, err);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/lib/firestore-particles.ts` around lines 158 - 167, The
current safeData function logs unparseable Particle documents with console.warn,
which can hide serious data corruption from error monitoring; update the
safeData function to use console.error instead of console.warn when catching
parse errors (keep the same message that includes snap.ref.path and the caught
err) so these events surface in error tracking for
QueryDocumentSnapshot<Particle> handling.

318-322: Use typedDoc here for consistent typing (converter doesn’t apply to updateDoc)

bumpParentLastChildCreatedAt uses updateDoc(doc(firestoreDb, parentDocPath), ...) while the rest of the file uses updateDoc(typedDoc(docPath), ...). Since Firebase FirestoreDataConverter isn’t used for updateDoc writes, this doesn’t change runtime conversion/validation—only TypeScript reference typing. Consider switching to updateDoc(typedDoc(parentDocPath), ...) for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/lib/firestore-particles.ts` around lines 318 - 322, The call
in bumpParentLastChildCreatedAt uses updateDoc(doc(firestoreDb, parentDocPath),
...) which bypasses the file's TypeScript converter typing; replace the first
argument with typedDoc(parentDocPath) (i.e., updateDoc(typedDoc(parentDocPath),
...)) so the FirestoreDataConverter types are applied for consistent typing
across this file, keeping the existing Timestamp.fromDate and the .catch()
behavior unchanged and ensuring any necessary imports for typedDoc remain.
js/desktop/src/features/particles/stream-bottom-bar.tsx (1)

42-53: Quick win

Trim structural JSX comments that restate the code.

These comments duplicate what component names already convey, which adds noise in a hot UI file. Keep comments only for non-obvious constraints/decisions.

As per coding guidelines, “Avoid non-essential comments in code; clean them up if present.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/particles/stream-bottom-bar.tsx` around lines 42 -
53, Remove the redundant JSX comments that merely restate component purpose
around the PlaybackPageIndicator and the following div (the lines starting with
"Presence avatars — above the blurred background" and "Blurred background
container — tracks + controls"); leave no comment for those obvious structural
elements and only retain comments where there's a non-obvious constraint or
decision, ensuring references to the PlaybackPageIndicator component and the
surrounding container/div (className="pb-3") are used to locate the removal
spots.

Source: Coding guidelines

js/desktop/src/features/particles/particle-children-list.tsx (1)

180-186: Quick win

Use a semantic row action primitive instead of clickable <div role="button">.

This interaction pattern is duplicated in three row components and reimplements native button behavior manually. Prefer a shared row wrapper built on shadcn Button (or native <button type="button">) to keep keyboard/focus behavior consistent and reduce duplicated key handling.

As per coding guidelines, "Use design system components from shadcn whenever possible in the Electron app."

Also applies to: 288-294, 346-352

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/particles/particle-children-list.tsx` around lines
180 - 186, Replace the non-semantic interactive div (role="button", tabIndex,
manual onKeyDown) with a proper button primitive from the design system (shadcn
Button) or a native <button type="button">: move the onClick handler
(onOpen(particle.id)) to the Button, remove tabIndex and manual key handling,
and preserve any styling/className and aria attributes; apply the same change to
the other duplicated row components in this file that use the same pattern so
keyboard/focus behavior is consistent.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/desktop/src/features/particles/container-view.tsx`:
- Around line 109-130: The modal is closed before the async create call
completes because setCreating(null) is called prior to awaiting
createParticle.mutateAsync/createStream.mutateAsync; move setCreating(null) to
after the await so the create step only closes on success, and wrap the mutation
in try/catch around the awaits (in the anonymous async handler where
`creating`/`setCreating` are used) so on error you leave `creating` intact or
reset appropriately and surface the error (e.g., rethrow or show an error)
before calling handleOpen(id).

In `@js/desktop/src/features/particles/stream-list-sidebar.tsx`:
- Around line 102-108: The Space-key handler on the clickable row (the div with
role="button" and onKeyDown) is not calling preventDefault/stopPropagation,
which allows the keypress to bubble to the global playback hotkeys in
use-playback-keys.ts; update the onKeyDown in stream-list-sidebar.tsx so that
when handling ' ' (Space) (and optionally 'Enter') you call e.preventDefault()
and e.stopPropagation() before invoking onClick to stop the event from reaching
the global listener, keeping the existing onClick call and accessibility
behavior intact.

In `@js/desktop/src/features/particles/stream-top-bar.tsx`:
- Around line 179-185: The current code rebuilds docPath using
particlePath(networkId, [streamParticle.id]) which ignores folder/container
nesting; instead accept/pass the resolved ParticlePath (streamPath) into TopBar
and replace the rebuild with const docPath = toFirestoreDocPath(streamPath) so
updateStreamStatus(docPath, isStreamOpen(streamParticle) ? 'closed' : 'open')
targets the correct Firestore document; update TopBar props/signature to include
streamPath and use that value in the updateStreamStatus call.

In `@js/desktop/src/features/particles/task-particle-view.tsx`:
- Around line 103-147: The handlers (handleToggleItem, handleCommitItemText,
handleRemoveItem, handleAddItem) read checklistRef.current and then call
writeChecklist which races with Firestore updates, so update the local base
before the round-trip: modify writeChecklist (or each handler) to first advance
checklistRef.current to the new items (e.g., checklistRef.current = items /
update the optimistic local checklist state) and then call
updateParticleProperties<'task'>(docPath, { checklist: items }); ensure
checklistRef is kept in sync so consecutive local edits compose correctly.
- Around line 167-170: The blur handler flips editing off even when the
portal-rendered assignee Select is open, so add local state (e.g.
isAssigneeOpen) and wire the assignee Select's onOpenChange to set that state,
then update the focus/blur suspension logic to also check isAssigneeOpen before
calling setEditing(false) (i.e. in onBlurCapture leave editing true if
isAssigneeOpen is true); apply the same change to the other similar block that
uses setEditing and onBlurCapture so the dropdown interaction doesn't unsuspend
playback.

In `@js/desktop/src/hooks/use-list-keyboard-nav.ts`:
- Around line 64-71: The Enter key handler currently calls onOpen(items[idx].id)
inside the functional updater passed to setSelectedIndex, which introduces a
side effect in a state updater; change the handler so setSelectedIndex's updater
only computes and returns the new index (no calls to onOpen), capture the
current idx in a local variable or compute it before/after calling
setSelectedIndex, then call onOpen(items[idx].id) outside the updater (ensuring
idx is not null and within items.length). Keep references to setSelectedIndex,
onOpen, items and the Enter key branch when moving the side effect.

In `@js/desktop/src/hooks/use-live-draft-field.ts`:
- Around line 34-42: The current logic advances prevRemote even while focused,
preventing a pending remote update from being applied on blur; change the sync
so you only call setPrevRemote(remoteValue) when not focused (or when there's no
local draft to flush) and leave prevRemote unchanged while focused so the
inequality stays true; also ensure your blur handler (or the focused state
transition in useLiveDraftField) checks if remoteValue !== prevRemote and, when
focus is lost and there is no local draft to keep, calls
setPrevRemote(remoteValue) and setValue(remoteValue) to apply the pending remote
update.

In `@js/desktop/src/hooks/use-stream-view-mode.ts`:
- Around line 32-37: The state updates for stream changes are happening during
render inside useStreamViewMode (the if block that calls setPrevStreamId and
setMode with prevStreamId/streamParticle.id), which is an anti-pattern; move
that logic into a useEffect: keep prevStreamId and mode state as-is but remove
the in-render if, and create a useEffect keyed on streamParticle.id (and userId
if decideMode depends on it) that compares prevStreamId to streamParticle.id and
calls setPrevStreamId(streamParticle.id) and setMode(decideMode(streamParticle,
userId)) when the id changes so updates occur after render and avoid extra
render churn and warnings.

---

Outside diff comments:
In `@js/desktop/src/features/compose/compose-overlay.tsx`:
- Around line 523-535: handleCancelIntent currently ignores the 'task' step so
store-driven cancel events routed via the compose-intent subscription are no-ops
while the task form is open; update the conditional in handleCancelIntent (the
function that reads stepRef.current) to treat 'task' the same as
'typing'/'configuring'/'picking' (i.e., call cancel() for 'task'), ensuring both
local Esc and shared store cancel paths close the task form; adjust any related
logic in the recording branch if needed and keep the useCallback dependencies
(cancel, cancelRecording, cancelScreenRecording) intact.

---

Nitpick comments:
In `@js/desktop/src/features/particles/particle-children-list.tsx`:
- Around line 180-186: Replace the non-semantic interactive div (role="button",
tabIndex, manual onKeyDown) with a proper button primitive from the design
system (shadcn Button) or a native <button type="button">: move the onClick
handler (onOpen(particle.id)) to the Button, remove tabIndex and manual key
handling, and preserve any styling/className and aria attributes; apply the same
change to the other duplicated row components in this file that use the same
pattern so keyboard/focus behavior is consistent.

In `@js/desktop/src/features/particles/stream-bottom-bar.tsx`:
- Around line 42-53: Remove the redundant JSX comments that merely restate
component purpose around the PlaybackPageIndicator and the following div (the
lines starting with "Presence avatars — above the blurred background" and
"Blurred background container — tracks + controls"); leave no comment for those
obvious structural elements and only retain comments where there's a non-obvious
constraint or decision, ensuring references to the PlaybackPageIndicator
component and the surrounding container/div (className="pb-3") are used to
locate the removal spots.

In `@js/desktop/src/lib/firestore-particles.ts`:
- Around line 158-167: The current safeData function logs unparseable Particle
documents with console.warn, which can hide serious data corruption from error
monitoring; update the safeData function to use console.error instead of
console.warn when catching parse errors (keep the same message that includes
snap.ref.path and the caught err) so these events surface in error tracking for
QueryDocumentSnapshot<Particle> handling.
- Around line 318-322: The call in bumpParentLastChildCreatedAt uses
updateDoc(doc(firestoreDb, parentDocPath), ...) which bypasses the file's
TypeScript converter typing; replace the first argument with
typedDoc(parentDocPath) (i.e., updateDoc(typedDoc(parentDocPath), ...)) so the
FirestoreDataConverter types are applied for consistent typing across this file,
keeping the existing Timestamp.fromDate and the .catch() behavior unchanged and
ensuring any necessary imports for typedDoc remain.
🪄 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: a498c1db-970d-4003-9719-40ebac518f56

📥 Commits

Reviewing files that changed from the base of the PR and between a358774106 and 224b64f86b.

📒 Files selected for processing (34)
  • js/desktop/src/api/types.ts
  • js/desktop/src/components/ui/textarea.tsx
  • js/desktop/src/features/compose/compose-overlay.tsx
  • js/desktop/src/features/compose/configure-container-step.tsx
  • js/desktop/src/features/compose/task-compose-step.tsx
  • js/desktop/src/features/layout.tsx
  • js/desktop/src/features/network-root.tsx
  • js/desktop/src/features/particles/container-view.tsx
  • js/desktop/src/features/particles/fallback-particle-view.tsx
  • js/desktop/src/features/particles/folder-view.tsx
  • js/desktop/src/features/particles/particle-children-list.tsx
  • js/desktop/src/features/particles/particle-preview.tsx
  • js/desktop/src/features/particles/particle-view-resolver.tsx
  • js/desktop/src/features/particles/stream-bottom-bar.tsx
  • js/desktop/src/features/particles/stream-context-menu.tsx
  • js/desktop/src/features/particles/stream-list-sidebar.tsx
  • js/desktop/src/features/particles/stream-top-bar.tsx
  • js/desktop/src/features/particles/stream-view.tsx
  • js/desktop/src/features/particles/task-particle-view.tsx
  • js/desktop/src/features/particles/text-particle-view.tsx
  • js/desktop/src/hooks/use-container-children.ts
  • js/desktop/src/hooks/use-create-particle.ts
  • js/desktop/src/hooks/use-fixed-dwell.ts
  • js/desktop/src/hooks/use-list-keyboard-nav.ts
  • js/desktop/src/hooks/use-live-draft-field.ts
  • js/desktop/src/hooks/use-playback-keys.ts
  • js/desktop/src/hooks/use-stream-navigation-keys.ts
  • js/desktop/src/hooks/use-stream-particles.ts
  • js/desktop/src/hooks/use-stream-playback.ts
  • js/desktop/src/hooks/use-stream-view-mode.ts
  • js/desktop/src/lib/firestore-particles.ts
  • js/desktop/src/lib/particle-display.ts
  • js/desktop/src/stores/compose-intent-store.ts
  • js/mobile/src/lib/firestore-particles.ts
💤 Files with no reviewable changes (1)
  • js/desktop/src/hooks/use-stream-particles.ts
**Actionable comments posted: 8** > [!CAUTION] > Some comments are outside the diff and can’t be posted inline due to platform limitations. > > > > <details> > <summary>⚠️ Outside diff range comments (1)</summary><blockquote> > > <details> > <summary>js/desktop/src/features/compose/compose-overlay.tsx (1)</summary><blockquote> > > `523-535`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ > > **Handle store-driven cancel actions in the task step.** > > `handleCancelIntent()` still skips `'task'`, so external `cancel` requests routed through the compose-intent subscription no-op while the task form is open. That makes the new task flow inconsistent with `typing`/`configuring`: local `Esc` works, but the shared store-driven cancel path does not. > > > <details> > <summary>Proposed fix</summary> > > ```diff > const handleCancelIntent = useCallback(() => { > const s = stepRef.current; > if (s === 'recording' || s === 'reviewing') { > if (recordingSourceRef.current === 'screen') { > cancelScreenRecording(); > } else { > cancelRecording(); > } > cancel(); > - } else if (s === 'typing' || s === 'configuring' || s === 'picking') { > + } else if ( > + s === 'typing' || > + s === 'task' || > + s === 'configuring' || > + s === 'picking' > + ) { > cancel(); > } > }, [cancel, cancelRecording, cancelScreenRecording]); > ``` > </details> > > <details> > <summary>🤖 Prompt for AI Agents</summary> > > ``` > Verify each finding against current code. Fix only still-valid issues, skip the > rest with a brief reason, keep changes minimal, and validate. > > In `@js/desktop/src/features/compose/compose-overlay.tsx` around lines 523 - 535, > handleCancelIntent currently ignores the 'task' step so store-driven cancel > events routed via the compose-intent subscription are no-ops while the task form > is open; update the conditional in handleCancelIntent (the function that reads > stepRef.current) to treat 'task' the same as 'typing'/'configuring'/'picking' > (i.e., call cancel() for 'task'), ensuring both local Esc and shared store > cancel paths close the task form; adjust any related logic in the recording > branch if needed and keep the useCallback dependencies (cancel, cancelRecording, > cancelScreenRecording) intact. > ``` > > </details> > > <!-- cr-comment:v1:63f1f0bc0c08fd8ee0c5d1cb --> > > </blockquote></details> > > </blockquote></details> <details> <summary>🧹 Nitpick comments (4)</summary><blockquote> <details> <summary>js/desktop/src/lib/firestore-particles.ts (2)</summary><blockquote> `158-167`: _⚡ Quick win_ **Consider using `console.error` for unparseable particles.** While the defensive pattern is excellent for resilience, unparseable particles indicate data corruption or schema drift that should surface prominently. Using `console.error` instead of `console.warn` ensures these issues are visible in error tracking and monitoring systems. <details> <summary>📊 Proposed change</summary> ```diff - console.warn(`Skipping unparseable particle at ${snap.ref.path}:`, err); + console.error(`Skipping unparseable particle at ${snap.ref.path}:`, err); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/lib/firestore-particles.ts` around lines 158 - 167, The current safeData function logs unparseable Particle documents with console.warn, which can hide serious data corruption from error monitoring; update the safeData function to use console.error instead of console.warn when catching parse errors (keep the same message that includes snap.ref.path and the caught err) so these events surface in error tracking for QueryDocumentSnapshot<Particle> handling. ``` </details> <!-- cr-comment:v1:f0b2afc469f239b0c945e196 --> --- `318-322`: **Use `typedDoc` here for consistent typing (converter doesn’t apply to `updateDoc`)** `bumpParentLastChildCreatedAt` uses `updateDoc(doc(firestoreDb, parentDocPath), ...)` while the rest of the file uses `updateDoc(typedDoc(docPath), ...)`. Since Firebase `FirestoreDataConverter` isn’t used for `updateDoc` writes, this doesn’t change runtime conversion/validation—only TypeScript reference typing. Consider switching to `updateDoc(typedDoc(parentDocPath), ...)` for consistency. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/lib/firestore-particles.ts` around lines 318 - 322, The call in bumpParentLastChildCreatedAt uses updateDoc(doc(firestoreDb, parentDocPath), ...) which bypasses the file's TypeScript converter typing; replace the first argument with typedDoc(parentDocPath) (i.e., updateDoc(typedDoc(parentDocPath), ...)) so the FirestoreDataConverter types are applied for consistent typing across this file, keeping the existing Timestamp.fromDate and the .catch() behavior unchanged and ensuring any necessary imports for typedDoc remain. ``` </details> <!-- cr-comment:v1:17acb742b6dbbc226cae41df --> </blockquote></details> <details> <summary>js/desktop/src/features/particles/stream-bottom-bar.tsx (1)</summary><blockquote> `42-53`: _⚡ Quick win_ **Trim structural JSX comments that restate the code.** These comments duplicate what component names already convey, which adds noise in a hot UI file. Keep comments only for non-obvious constraints/decisions. As per coding guidelines, “Avoid non-essential comments in code; clean them up if present.” <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/stream-bottom-bar.tsx` around lines 42 - 53, Remove the redundant JSX comments that merely restate component purpose around the PlaybackPageIndicator and the following div (the lines starting with "Presence avatars — above the blurred background" and "Blurred background container — tracks + controls"); leave no comment for those obvious structural elements and only retain comments where there's a non-obvious constraint or decision, ensuring references to the PlaybackPageIndicator component and the surrounding container/div (className="pb-3") are used to locate the removal spots. ``` </details> <!-- cr-comment:v1:c2019644c0742ba3de23f7f4 --> _Source: Coding guidelines_ </blockquote></details> <details> <summary>js/desktop/src/features/particles/particle-children-list.tsx (1)</summary><blockquote> `180-186`: _⚡ Quick win_ **Use a semantic row action primitive instead of clickable `<div role="button">`.** This interaction pattern is duplicated in three row components and reimplements native button behavior manually. Prefer a shared row wrapper built on shadcn `Button` (or native `<button type="button">`) to keep keyboard/focus behavior consistent and reduce duplicated key handling. As per coding guidelines, "Use design system components from shadcn whenever possible in the Electron app." Also applies to: 288-294, 346-352 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/particle-children-list.tsx` around lines 180 - 186, Replace the non-semantic interactive div (role="button", tabIndex, manual onKeyDown) with a proper button primitive from the design system (shadcn Button) or a native <button type="button">: move the onClick handler (onOpen(particle.id)) to the Button, remove tabIndex and manual key handling, and preserve any styling/className and aria attributes; apply the same change to the other duplicated row components in this file that use the same pattern so keyboard/focus behavior is consistent. ``` </details> <!-- cr-comment:v1:b1da4994a31b305b478108cd --> _Source: Coding guidelines_ </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. Inline comments: In `@js/desktop/src/features/particles/container-view.tsx`: - Around line 109-130: The modal is closed before the async create call completes because setCreating(null) is called prior to awaiting createParticle.mutateAsync/createStream.mutateAsync; move setCreating(null) to after the await so the create step only closes on success, and wrap the mutation in try/catch around the awaits (in the anonymous async handler where `creating`/`setCreating` are used) so on error you leave `creating` intact or reset appropriately and surface the error (e.g., rethrow or show an error) before calling handleOpen(id). In `@js/desktop/src/features/particles/stream-list-sidebar.tsx`: - Around line 102-108: The Space-key handler on the clickable row (the div with role="button" and onKeyDown) is not calling preventDefault/stopPropagation, which allows the keypress to bubble to the global playback hotkeys in use-playback-keys.ts; update the onKeyDown in stream-list-sidebar.tsx so that when handling ' ' (Space) (and optionally 'Enter') you call e.preventDefault() and e.stopPropagation() before invoking onClick to stop the event from reaching the global listener, keeping the existing onClick call and accessibility behavior intact. In `@js/desktop/src/features/particles/stream-top-bar.tsx`: - Around line 179-185: The current code rebuilds docPath using particlePath(networkId, [streamParticle.id]) which ignores folder/container nesting; instead accept/pass the resolved ParticlePath (streamPath) into TopBar and replace the rebuild with const docPath = toFirestoreDocPath(streamPath) so updateStreamStatus(docPath, isStreamOpen(streamParticle) ? 'closed' : 'open') targets the correct Firestore document; update TopBar props/signature to include streamPath and use that value in the updateStreamStatus call. In `@js/desktop/src/features/particles/task-particle-view.tsx`: - Around line 103-147: The handlers (handleToggleItem, handleCommitItemText, handleRemoveItem, handleAddItem) read checklistRef.current and then call writeChecklist which races with Firestore updates, so update the local base before the round-trip: modify writeChecklist (or each handler) to first advance checklistRef.current to the new items (e.g., checklistRef.current = items / update the optimistic local checklist state) and then call updateParticleProperties<'task'>(docPath, { checklist: items }); ensure checklistRef is kept in sync so consecutive local edits compose correctly. - Around line 167-170: The blur handler flips editing off even when the portal-rendered assignee Select is open, so add local state (e.g. isAssigneeOpen) and wire the assignee Select's onOpenChange to set that state, then update the focus/blur suspension logic to also check isAssigneeOpen before calling setEditing(false) (i.e. in onBlurCapture leave editing true if isAssigneeOpen is true); apply the same change to the other similar block that uses setEditing and onBlurCapture so the dropdown interaction doesn't unsuspend playback. In `@js/desktop/src/hooks/use-list-keyboard-nav.ts`: - Around line 64-71: The Enter key handler currently calls onOpen(items[idx].id) inside the functional updater passed to setSelectedIndex, which introduces a side effect in a state updater; change the handler so setSelectedIndex's updater only computes and returns the new index (no calls to onOpen), capture the current idx in a local variable or compute it before/after calling setSelectedIndex, then call onOpen(items[idx].id) outside the updater (ensuring idx is not null and within items.length). Keep references to setSelectedIndex, onOpen, items and the Enter key branch when moving the side effect. In `@js/desktop/src/hooks/use-live-draft-field.ts`: - Around line 34-42: The current logic advances prevRemote even while focused, preventing a pending remote update from being applied on blur; change the sync so you only call setPrevRemote(remoteValue) when not focused (or when there's no local draft to flush) and leave prevRemote unchanged while focused so the inequality stays true; also ensure your blur handler (or the focused state transition in useLiveDraftField) checks if remoteValue !== prevRemote and, when focus is lost and there is no local draft to keep, calls setPrevRemote(remoteValue) and setValue(remoteValue) to apply the pending remote update. In `@js/desktop/src/hooks/use-stream-view-mode.ts`: - Around line 32-37: The state updates for stream changes are happening during render inside useStreamViewMode (the if block that calls setPrevStreamId and setMode with prevStreamId/streamParticle.id), which is an anti-pattern; move that logic into a useEffect: keep prevStreamId and mode state as-is but remove the in-render if, and create a useEffect keyed on streamParticle.id (and userId if decideMode depends on it) that compares prevStreamId to streamParticle.id and calls setPrevStreamId(streamParticle.id) and setMode(decideMode(streamParticle, userId)) when the id changes so updates occur after render and avoid extra render churn and warnings. --- Outside diff comments: In `@js/desktop/src/features/compose/compose-overlay.tsx`: - Around line 523-535: handleCancelIntent currently ignores the 'task' step so store-driven cancel events routed via the compose-intent subscription are no-ops while the task form is open; update the conditional in handleCancelIntent (the function that reads stepRef.current) to treat 'task' the same as 'typing'/'configuring'/'picking' (i.e., call cancel() for 'task'), ensuring both local Esc and shared store cancel paths close the task form; adjust any related logic in the recording branch if needed and keep the useCallback dependencies (cancel, cancelRecording, cancelScreenRecording) intact. --- Nitpick comments: In `@js/desktop/src/features/particles/particle-children-list.tsx`: - Around line 180-186: Replace the non-semantic interactive div (role="button", tabIndex, manual onKeyDown) with a proper button primitive from the design system (shadcn Button) or a native <button type="button">: move the onClick handler (onOpen(particle.id)) to the Button, remove tabIndex and manual key handling, and preserve any styling/className and aria attributes; apply the same change to the other duplicated row components in this file that use the same pattern so keyboard/focus behavior is consistent. In `@js/desktop/src/features/particles/stream-bottom-bar.tsx`: - Around line 42-53: Remove the redundant JSX comments that merely restate component purpose around the PlaybackPageIndicator and the following div (the lines starting with "Presence avatars — above the blurred background" and "Blurred background container — tracks + controls"); leave no comment for those obvious structural elements and only retain comments where there's a non-obvious constraint or decision, ensuring references to the PlaybackPageIndicator component and the surrounding container/div (className="pb-3") are used to locate the removal spots. In `@js/desktop/src/lib/firestore-particles.ts`: - Around line 158-167: The current safeData function logs unparseable Particle documents with console.warn, which can hide serious data corruption from error monitoring; update the safeData function to use console.error instead of console.warn when catching parse errors (keep the same message that includes snap.ref.path and the caught err) so these events surface in error tracking for QueryDocumentSnapshot<Particle> handling. - Around line 318-322: The call in bumpParentLastChildCreatedAt uses updateDoc(doc(firestoreDb, parentDocPath), ...) which bypasses the file's TypeScript converter typing; replace the first argument with typedDoc(parentDocPath) (i.e., updateDoc(typedDoc(parentDocPath), ...)) so the FirestoreDataConverter types are applied for consistent typing across this file, keeping the existing Timestamp.fromDate and the .catch() behavior unchanged and ensuring any necessary imports for typedDoc remain. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `a498c1db-970d-4003-9719-40ebac518f56` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between a358774106ae52274a93ba24ac709367fd1270f7 and 224b64f86b013496a80f3ef8e0f420cf34d816fc. </details> <details> <summary>📒 Files selected for processing (34)</summary> * `js/desktop/src/api/types.ts` * `js/desktop/src/components/ui/textarea.tsx` * `js/desktop/src/features/compose/compose-overlay.tsx` * `js/desktop/src/features/compose/configure-container-step.tsx` * `js/desktop/src/features/compose/task-compose-step.tsx` * `js/desktop/src/features/layout.tsx` * `js/desktop/src/features/network-root.tsx` * `js/desktop/src/features/particles/container-view.tsx` * `js/desktop/src/features/particles/fallback-particle-view.tsx` * `js/desktop/src/features/particles/folder-view.tsx` * `js/desktop/src/features/particles/particle-children-list.tsx` * `js/desktop/src/features/particles/particle-preview.tsx` * `js/desktop/src/features/particles/particle-view-resolver.tsx` * `js/desktop/src/features/particles/stream-bottom-bar.tsx` * `js/desktop/src/features/particles/stream-context-menu.tsx` * `js/desktop/src/features/particles/stream-list-sidebar.tsx` * `js/desktop/src/features/particles/stream-top-bar.tsx` * `js/desktop/src/features/particles/stream-view.tsx` * `js/desktop/src/features/particles/task-particle-view.tsx` * `js/desktop/src/features/particles/text-particle-view.tsx` * `js/desktop/src/hooks/use-container-children.ts` * `js/desktop/src/hooks/use-create-particle.ts` * `js/desktop/src/hooks/use-fixed-dwell.ts` * `js/desktop/src/hooks/use-list-keyboard-nav.ts` * `js/desktop/src/hooks/use-live-draft-field.ts` * `js/desktop/src/hooks/use-playback-keys.ts` * `js/desktop/src/hooks/use-stream-navigation-keys.ts` * `js/desktop/src/hooks/use-stream-particles.ts` * `js/desktop/src/hooks/use-stream-playback.ts` * `js/desktop/src/hooks/use-stream-view-mode.ts` * `js/desktop/src/lib/firestore-particles.ts` * `js/desktop/src/lib/particle-display.ts` * `js/desktop/src/stores/compose-intent-store.ts` * `js/mobile/src/lib/firestore-particles.ts` </details> <details> <summary>💤 Files with no reviewable changes (1)</summary> * js/desktop/src/hooks/use-stream-particles.ts </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
@@ -0,0 +127,4 @@
visibleTo,
});
handleOpen(id);
},
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:18 +00:00

⚠️ Potential issue | 🟠 Major | Quick win

Close the create step only after mutation succeeds.

On Line 112, setCreating(null) runs before the async create call. A failed mutation closes the modal and drops the user out of the flow.

Proposed fix
   const handleCreateContainer = useCallback(
     async (name: string, visibleTo: string[]) => {
       if (!userId || !creating) return;
       const kind = creating;
-      setCreating(null);
       const id =
         kind === 'folder'
           ? await createParticle.mutateAsync({
               path,
               type: 'folder',
               properties: { name },
               createdByHumanId: userId,
               visibleTo,
             })
           : await createStream.mutateAsync({
               networkId,
               parentPath: path,
               properties: { name },
               createdByHumanId: userId,
               visibleTo,
             });
+      setCreating(null);
       handleOpen(id);
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/particles/container-view.tsx` around lines 109 - 130,
The modal is closed before the async create call completes because
setCreating(null) is called prior to awaiting
createParticle.mutateAsync/createStream.mutateAsync; move setCreating(null) to
after the await so the create step only closes on success, and wrap the mutation
in try/catch around the awaits (in the anonymous async handler where
`creating`/`setCreating` are used) so on error you leave `creating` intact or
reset appropriately and surface the error (e.g., rethrow or show an error)
before calling handleOpen(id).
_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Close the create step only after mutation succeeds.** On Line 112, `setCreating(null)` runs before the async create call. A failed mutation closes the modal and drops the user out of the flow. <details> <summary>Proposed fix</summary> ```diff const handleCreateContainer = useCallback( async (name: string, visibleTo: string[]) => { if (!userId || !creating) return; const kind = creating; - setCreating(null); const id = kind === 'folder' ? await createParticle.mutateAsync({ path, type: 'folder', properties: { name }, createdByHumanId: userId, visibleTo, }) : await createStream.mutateAsync({ networkId, parentPath: path, properties: { name }, createdByHumanId: userId, visibleTo, }); + setCreating(null); handleOpen(id); }, ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/container-view.tsx` around lines 109 - 130, The modal is closed before the async create call completes because setCreating(null) is called prior to awaiting createParticle.mutateAsync/createStream.mutateAsync; move setCreating(null) to after the await so the create step only closes on success, and wrap the mutation in try/catch around the awaits (in the anonymous async handler where `creating`/`setCreating` are used) so on error you leave `creating` intact or reset appropriately and surface the error (e.g., rethrow or show an error) before calling handleOpen(id). ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:24edae63b996c2744eef5d54 --> <!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +106,4 @@
// Enter only — Space is reserved for play/pause in the stream view.
onKeyDown={(e) => {
if (e.key === 'Enter') onClick();
}}
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:18 +00:00

⚠️ Potential issue | 🟠 Major | Quick win

Space-key row selection leaks into global playback hotkeys.

Line 107 handles Space on a div-button without preventing default/propagation. Combined with the global Space listener in use-playback-keys.ts (Line 56), keyboard row selection can also toggle playback pause.

🐛 Suggested fix
-    <div
-      role="button"
-      tabIndex={0}
-      onClick={onClick}
-      onKeyDown={(e) => {
-        if (e.key === 'Enter' || e.key === ' ') onClick();
-      }}
+    <button
+      type="button"
+      onClick={onClick}
       className={cn(
         'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors',
         isSelected ? 'bg-white/10' : 'hover:bg-white/5',
       )}
     >
@@
-    </div>
+    </button>
📝 Committable suggestion

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

    <button
      type="button"
      onClick={onClick}
      className={cn(
        'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors',
        isSelected ? 'bg-white/10' : 'hover:bg-white/5',
      )}
    >
      {/* existing children content */}
    </button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/particles/stream-list-sidebar.tsx` around lines 102 -
108, The Space-key handler on the clickable row (the div with role="button" and
onKeyDown) is not calling preventDefault/stopPropagation, which allows the
keypress to bubble to the global playback hotkeys in use-playback-keys.ts;
update the onKeyDown in stream-list-sidebar.tsx so that when handling ' '
(Space) (and optionally 'Enter') you call e.preventDefault() and
e.stopPropagation() before invoking onClick to stop the event from reaching the
global listener, keeping the existing onClick call and accessibility behavior
intact.
_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Space-key row selection leaks into global playback hotkeys.** Line 107 handles Space on a `div`-button without preventing default/propagation. Combined with the global Space listener in `use-playback-keys.ts` (Line 56), keyboard row selection can also toggle playback pause. <details> <summary>🐛 Suggested fix</summary> ```diff - <div - role="button" - tabIndex={0} - onClick={onClick} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') onClick(); - }} + <button + type="button" + onClick={onClick} className={cn( 'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors', isSelected ? 'bg-white/10' : 'hover:bg-white/5', )} > @@ - </div> + </button> ``` </details> <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion <button type="button" onClick={onClick} className={cn( 'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors', isSelected ? 'bg-white/10' : 'hover:bg-white/5', )} > {/* existing children content */} </button> ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/stream-list-sidebar.tsx` around lines 102 - 108, The Space-key handler on the clickable row (the div with role="button" and onKeyDown) is not calling preventDefault/stopPropagation, which allows the keypress to bubble to the global playback hotkeys in use-playback-keys.ts; update the onKeyDown in stream-list-sidebar.tsx so that when handling ' ' (Space) (and optionally 'Enter') you call e.preventDefault() and e.stopPropagation() before invoking onClick to stop the event from reaching the global listener, keeping the existing onClick call and accessibility behavior intact. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:9c48e34d6f3c96fbe1b1c15d --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:18 +00:00

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Use the resolved stream path when updating stream status.

docPath is rebuilt as particlePath(networkId, [streamParticle.id]), which always points to a root child path. With folder/container nesting now supported, a stream can live under a parent path, so this update can target the wrong Firestore document. Pass the current stream ParticlePath into TopBar and call toFirestoreDocPath(streamPath) directly.

Suggested direction
-interface TopBarProps {
+interface TopBarProps {
   networkId: string;
   particle: Particle | null;
   streamParticle: Particle & { type: 'stream' };
+  streamPath: ParticlePath;
 }
 
-export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
+export function TopBar({ networkId, particle, streamParticle, streamPath }: TopBarProps) {
 ...
-              const docPath = toFirestoreDocPath(
-                particlePath(networkId, [streamParticle.id]),
-              );
+              const docPath = toFirestoreDocPath(streamPath);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/particles/stream-top-bar.tsx` around lines 179 - 185,
The current code rebuilds docPath using particlePath(networkId,
[streamParticle.id]) which ignores folder/container nesting; instead accept/pass
the resolved ParticlePath (streamPath) into TopBar and replace the rebuild with
const docPath = toFirestoreDocPath(streamPath) so updateStreamStatus(docPath,
isStreamOpen(streamParticle) ? 'closed' : 'open') targets the correct Firestore
document; update TopBar props/signature to include streamPath and use that value
in the updateStreamStatus call.

Addressed in commit 044f1a7

_⚠️ Potential issue_ | _🟠 Major_ | _🏗️ Heavy lift_ **Use the resolved stream path when updating stream status.** `docPath` is rebuilt as `particlePath(networkId, [streamParticle.id])`, which always points to a root child path. With folder/container nesting now supported, a stream can live under a parent path, so this update can target the wrong Firestore document. Pass the current stream `ParticlePath` into `TopBar` and call `toFirestoreDocPath(streamPath)` directly. <details> <summary>Suggested direction</summary> ```diff -interface TopBarProps { +interface TopBarProps { networkId: string; particle: Particle | null; streamParticle: Particle & { type: 'stream' }; + streamPath: ParticlePath; } -export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { +export function TopBar({ networkId, particle, streamParticle, streamPath }: TopBarProps) { ... - const docPath = toFirestoreDocPath( - particlePath(networkId, [streamParticle.id]), - ); + const docPath = toFirestoreDocPath(streamPath); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/stream-top-bar.tsx` around lines 179 - 185, The current code rebuilds docPath using particlePath(networkId, [streamParticle.id]) which ignores folder/container nesting; instead accept/pass the resolved ParticlePath (streamPath) into TopBar and replace the rebuild with const docPath = toFirestoreDocPath(streamPath) so updateStreamStatus(docPath, isStreamOpen(streamParticle) ? 'closed' : 'open') targets the correct Firestore document; update TopBar props/signature to include streamPath and use that value in the updateStreamStatus call. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:862c0ef3910a220c00528f9b --> <!-- This is an auto-generated comment by CodeRabbit --> ✅ Addressed in commit 044f1a7
@@ -0,0 +148,4 @@
void writeChecklist([...checklistRef.current, { text, done: false }]);
},
[writeChecklist],
);
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:18 +00:00

⚠️ Potential issue | 🟠 Major | Quick win

Make writeChecklist advance the local base state before the Firestore round-trip.

All four checklist handlers branch from checklistRef.current, but that ref is only refreshed by the next snapshot in Lines 99-101. Because updateParticleProperties() writes the entire properties.checklist field, two quick local edits under normal latency will both derive from the same stale array and the later write can drop the earlier change. Update the ref before issuing the write, or keep an optimistic local checklist state, so consecutive edits compose locally.

Suggested fix
  const writeChecklist = useCallback(
-    (items: ChecklistItem[]) =>
-      updateParticleProperties<'task'>(docPath, { checklist: items }),
+    (items: ChecklistItem[]) => {
+      checklistRef.current = items;
+      return updateParticleProperties<'task'>(docPath, { checklist: items });
+    },
    [docPath],
  );
📝 Committable suggestion

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

  const writeChecklist = useCallback(
    (items: ChecklistItem[]) => {
      checklistRef.current = items;
      return updateParticleProperties<'task'>(docPath, { checklist: items });
    },
    [docPath],
  );

  const handleToggleDone = useCallback(
    (checked: boolean) =>
      updateParticleProperties<'task'>(docPath, { done: checked }),
    [docPath],
  );

  const handleToggleItem = useCallback(
    (index: number, checked: boolean) => {
      const items = checklistRef.current.map((item, i) =>
        i === index ? { ...item, done: checked } : item,
      );
      void writeChecklist(items);
    },
    [writeChecklist],
  );

  const handleCommitItemText = useCallback(
    (index: number, text: string) => {
      const items = checklistRef.current.map((item, i) =>
        i === index ? { ...item, text } : item,
      );
      void writeChecklist(items);
    },
    [writeChecklist],
  );

  const handleRemoveItem = useCallback(
    (index: number) => {
      void writeChecklist(checklistRef.current.filter((_, i) => i !== index));
    },
    [writeChecklist],
  );

  const handleAddItem = useCallback(
    (text: string) => {
      void writeChecklist([...checklistRef.current, { text, done: false }]);
    },
    [writeChecklist],
  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/particles/task-particle-view.tsx` around lines 103 -
147, The handlers (handleToggleItem, handleCommitItemText, handleRemoveItem,
handleAddItem) read checklistRef.current and then call writeChecklist which
races with Firestore updates, so update the local base before the round-trip:
modify writeChecklist (or each handler) to first advance checklistRef.current to
the new items (e.g., checklistRef.current = items / update the optimistic local
checklist state) and then call updateParticleProperties<'task'>(docPath, {
checklist: items }); ensure checklistRef is kept in sync so consecutive local
edits compose correctly.

Addressed in commit 044f1a7

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Make `writeChecklist` advance the local base state before the Firestore round-trip.** All four checklist handlers branch from `checklistRef.current`, but that ref is only refreshed by the next snapshot in Lines 99-101. Because `updateParticleProperties()` writes the entire `properties.checklist` field, two quick local edits under normal latency will both derive from the same stale array and the later write can drop the earlier change. Update the ref before issuing the write, or keep an optimistic local checklist state, so consecutive edits compose locally. <details> <summary>Suggested fix</summary> ```diff const writeChecklist = useCallback( - (items: ChecklistItem[]) => - updateParticleProperties<'task'>(docPath, { checklist: items }), + (items: ChecklistItem[]) => { + checklistRef.current = items; + return updateParticleProperties<'task'>(docPath, { checklist: items }); + }, [docPath], ); ``` </details> <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion const writeChecklist = useCallback( (items: ChecklistItem[]) => { checklistRef.current = items; return updateParticleProperties<'task'>(docPath, { checklist: items }); }, [docPath], ); const handleToggleDone = useCallback( (checked: boolean) => updateParticleProperties<'task'>(docPath, { done: checked }), [docPath], ); const handleToggleItem = useCallback( (index: number, checked: boolean) => { const items = checklistRef.current.map((item, i) => i === index ? { ...item, done: checked } : item, ); void writeChecklist(items); }, [writeChecklist], ); const handleCommitItemText = useCallback( (index: number, text: string) => { const items = checklistRef.current.map((item, i) => i === index ? { ...item, text } : item, ); void writeChecklist(items); }, [writeChecklist], ); const handleRemoveItem = useCallback( (index: number) => { void writeChecklist(checklistRef.current.filter((_, i) => i !== index)); }, [writeChecklist], ); const handleAddItem = useCallback( (text: string) => { void writeChecklist([...checklistRef.current, { text, done: false }]); }, [writeChecklist], ); ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/task-particle-view.tsx` around lines 103 - 147, The handlers (handleToggleItem, handleCommitItemText, handleRemoveItem, handleAddItem) read checklistRef.current and then call writeChecklist which races with Firestore updates, so update the local base before the round-trip: modify writeChecklist (or each handler) to first advance checklistRef.current to the new items (e.g., checklistRef.current = items / update the optimistic local checklist state) and then call updateParticleProperties<'task'>(docPath, { checklist: items }); ensure checklistRef is kept in sync so consecutive local edits compose correctly. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- cr-comment:v1:9e22918085088dd0eff33910 --> <!-- This is an auto-generated comment by CodeRabbit --> ✅ Addressed in commit 044f1a7
@@ -0,0 +171,4 @@
onFocusCapture={() => setEditing(true)}
onBlurCapture={(e) => {
if (!e.currentTarget.contains(e.relatedTarget)) setEditing(false);
}}
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:18 +00:00

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant sections of the file under review
sed -n '130,270p' js/desktop/src/features/particles/task-particle-view.tsx

# Locate and inspect the Select component used for portals/open state handling
sed -n '1,240p' js/desktop/src/components/ui/select.tsx

# Also search for the useSuspendPlayback hook signature/usages (to confirm what "editing" should represent)
rg -n "useSuspendPlayback" -S js/desktop/src | head -n 50
rg -n "task-edit-" -S js/desktop/src | head -n 50

Repository: flowy-live/llink

Length of output: 14569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# task-particle-view: inspect state/hooks around editing + useSuspendPlayback
sed -n '1,120p' js/desktop/src/features/particles/task-particle-view.tsx
sed -n '120,210p' js/desktop/src/features/particles/task-particle-view.tsx

# useSuspendPlayback hook implementation
sed -n '1,200p' js/desktop/src/hooks/use-suspend-playback.ts

# Look for other patterns of tracking open state for “open portals”
sed -n '1,120p' js/desktop/src/features/particles/text-reaction-input.tsx
sed -n '1,140p' js/desktop/src/features/attachments/attachment-lightbox.tsx

Repository: flowy-live/llink

Length of output: 14935


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# inspect playback-pause-store to see what happens when suspended ids are active
sed -n '1,220p' js/desktop/src/stores/playback-pause-store.ts

# inspect where suspension affects playback / global key handlers
rg -n "playback-pause-store|usePlaybackPauseStore" js/desktop/src | head -n 50
rg -n "isPaused|paused.*playback|pause.*playback|playbackPause" js/desktop/src | head -n 50

# inspect task-particle-view around editing + blur handling lines
nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,210p'

# inspect for Radix Select open usage elsewhere in repo (patterns similar to suggested fix)
rg -n "onOpenChange={.*set.*Open|onOpenChange" js/desktop/src/features js/desktop/src/components | head -n 50
rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 50

Repository: flowy-live/llink

Length of output: 2310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' js/desktop/src/stores/playback-pause-store.ts
rg -n "usePlaybackPauseStore" js/desktop/src | head -n 100

nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '1,140p'
nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,240p'

rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 200
rg -n "onOpenChange" js/desktop/src -S | head -n 200

Repository: flowy-live/llink

Length of output: 1915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' js/desktop/src/stores/playback-pause-store.ts
rg -n "usePlaybackPauseStore" js/desktop/src | head -n 100

nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '1,140p'
nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,240p'

rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 200
rg -n "onOpenChange" js/desktop/src -S | head -n 200

Repository: flowy-live/llink

Length of output: 1915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' js/desktop/src/stores/playback-pause-store.ts
rg -n "usePlaybackPauseStore" js/desktop/src | head -n 100

nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '1,140p'
nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,240p'

rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 200
rg -n "onOpenChange" js/desktop/src -S | head -n 200

Repository: flowy-live/llink

Length of output: 1915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# task-particle-view: show the exact relevant sections with line numbers
cat -n js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,210p'
cat -n js/desktop/src/features/particles/task-particle-view.tsx | sed -n '210,270p'

# playback suspension effects: where paused state is checked in key handlers
sed -n '1,220p' js/desktop/src/hooks/use-stream-action-keys.ts
sed -n '1,220p' js/desktop/src/hooks/use-playback-keys.ts
sed -n '240,360p' js/desktop/src/features/particles/stream-view.tsx

Repository: flowy-live/llink

Length of output: 14647


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "\\bpaused\\b" js/desktop/src/features/particles/stream-view.tsx | head -n 80
rg -n "useExitCountdown|useFixedDwell|dwell|playback" js/desktop/src/features/particles/stream-view.tsx | head -n 120

Repository: flowy-live/llink

Length of output: 745


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# show editing + suspender wiring in task-particle-view
cat -n js/desktop/src/features/particles/task-particle-view.tsx | sed -n '50,110p'

# inspect use-fixed-dwell to see how its `paused` prop affects dwell/playback behavior
sed -n '1,220p' js/desktop/src/hooks/use-fixed-dwell.ts

# confirm select content uses Radix portal + wrapper forwards Root props (for onOpenChange)
rg -n "SelectPrimitive\\.Portal|function Select\\(" js/desktop/src/components/ui/select.tsx

Repository: flowy-live/llink

Length of output: 3413


🌐 Web query:

Radix UI Select onOpenChange focus moved to content portal keyboard focus behavior

💡 Result:

In Radix UI Select, focus behavior when the dropdown opens is mainly controlled by the Select implementation (including its portalized content) rather than directly by onOpenChange. When Select.Content is rendered in a portal, focus “within” semantics and some focus-related heuristics can differ from non-portal DOM placement, which leads to quirks like “parent loses focus-within” and, in some container setups (e.g., Dialog/Drawer), missing or inconsistent focus on the first item. 1) onOpenChange vs focus: what actually moves focus - onOpenChange is just the controlled open state callback; Select’s internal focus management happens at open/close time in the Select component code. - Radix’s own Select docs describe keyboard behavior when opening/closing, including that focus is moved to items and returned to the trigger (e.g., Space/Enter opens and focuses the selected/first item; Esc closes and moves focus back to the trigger) [1]. - The Select docs also show that the content “pops out” and is rendered into a portal (defaulting to document.body) [1][2]. 2) Why portalization changes focus interactions (the core issue behind the reported behavior) - A known quirk is that ancestors can lose :focus-within when Select is opened because Select.Content is rendered in a portal (so the content is not within the ancestor’s DOM tree) [3]. This is consistent with browser semantics: :focus-within depends on DOM ancestry, not just global focus state. 3) Real-world symptoms with focus not landing on the first item inside overlay containers - There are multiple Radix issues where Select used inside higher-level primitives (Dialog/Drawer) does not focus items as expected on open. - Example: when Select is nested within a Dialog, the first SelectItem may not be automatically focused, breaking typeahead/arrow navigation [4]. - Example: inside a Drawer, only the trigger is focusable; opening the Select content does not move focus to the items, so arrow-key navigation doesn’t work [5]. - These reports strongly indicate that “content portal keyboard focus behavior” can be affected by the parent container’s focus management (focus trap, focus restoration, overlay behaviors), even though Select itself intends to focus the appropriate item when opened [1][5]. 4) What this means for your specific query (“focus moved to content portal keyboard focus behavior”) - If by “focus moved to content portal” you mean: “when Select.Content is portal-rendered, focus/keyboard interaction does/doesn’t go into the portal content,” then the evidence shows: - Select.Content is portal-rendered by default [1][2]. - Because it’s portal-rendered, DOM-based focus rules like :focus-within for ancestors can change [3]. - Additionally, in certain overlay contexts (Dialog/Drawer), Select may fail to focus the first item on open, which is observable as broken keyboard navigation [4][5]. If you tell me which Radix version(s) you’re using and what container you render Select inside (Dialog, Drawer, custom portal, etc.), I can narrow down which focus path is failing and what configuration/workaround to apply (e.g., focusing the trigger vs. ensuring focus trapping doesn’t swallow the initial item focus).

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "<Select\\b" js/desktop/src | head -n 80
rg -n "onOpenChange\\s*=\\s*\\{\\s*set.*Open|onOpenChange\\s*=\\s*\\w+Set|onOpenChange\\s*=\\s*set" js/desktop/src/components js/desktop/src/features -S | head -n 80
rg -n "SelectPrimitive\\.Root" js/desktop/src/components/ui/select.tsx

Repository: flowy-live/llink

Length of output: 1055


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "onBlurCapture=\\{\\(e\\) => \\{\\s*if \\(!e\\.currentTarget\\.contains\\(e\\.relatedTarget\\)\\) setEditing\\(false\\);\\s*\\}\\}" -S js/desktop/src || true
rg -n "onBlurCapture=.*contains\\(e\\.relatedTarget\\)" -S js/desktop/src | head -n 50
rg -n "useSuspendPlayback\\(.*Select|assigneeMenuOpen|onOpenChange=.*useSuspendPlayback" -S js/desktop/src || true

Repository: flowy-live/llink

Length of output: 42


🏁 Script executed:

rg -n "onBlurCapture" js/desktop/src | head -n 80
rg -n "relatedTarget" js/desktop/src | head -n 80

Repository: flowy-live/llink

Length of output: 291


Suspend playback while the assignee Select menu is open

task-particle-view.tsx flips editing to false on card blur using a DOM containment check; meanwhile the assignee <Select> menu is portal-rendered (SelectPrimitive.Portal), so focus can move outside the card while the dropdown is open—un-suspending playback and allowing the dwell timer to resume during menu interaction. Track the Select’s open state via onOpenChange and include it in the suspender condition.

File: js/desktop/src/features/particles/task-particle-view.tsx
Lines: 167-170

Snippet showing the final state of code at these lines

The below code does not show removed or replaced code.

        onFocusCapture={() => setEditing(true)}
        onBlurCapture={(e) => {
          if (!e.currentTarget.contains(e.relatedTarget)) setEditing(false);
        }}

Comment

Suggested fix
-  const [editing, setEditing] = useState(false);
-  useSuspendPlayback(editing, `task-edit-${particle.id}`);
+  const [editing, setEditing] = useState(false);
+  const [assigneeMenuOpen, setAssigneeMenuOpen] = useState(false);
+  useSuspendPlayback(
+    editing || assigneeMenuOpen,
+    `task-edit-${particle.id}`,
+  );
-          <Select
+          <Select
+            onOpenChange={setAssigneeMenuOpen}
             value={assigned_to ?? UNASSIGNED}
             onValueChange={handleAssign}
           >

Also applies to: 222-247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/features/particles/task-particle-view.tsx` around lines 167 -
170, The blur handler flips editing off even when the portal-rendered assignee
Select is open, so add local state (e.g. isAssigneeOpen) and wire the assignee
Select's onOpenChange to set that state, then update the focus/blur suspension
logic to also check isAssigneeOpen before calling setEditing(false) (i.e. in
onBlurCapture leave editing true if isAssigneeOpen is true); apply the same
change to the other similar block that uses setEditing and onBlurCapture so the
dropdown interaction doesn't unsuspend playback.
_⚠️ Potential issue_ | _🟠 Major_ <details> <summary>🧩 Analysis chain</summary> 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # Show relevant sections of the file under review sed -n '130,270p' js/desktop/src/features/particles/task-particle-view.tsx # Locate and inspect the Select component used for portals/open state handling sed -n '1,240p' js/desktop/src/components/ui/select.tsx # Also search for the useSuspendPlayback hook signature/usages (to confirm what "editing" should represent) rg -n "useSuspendPlayback" -S js/desktop/src | head -n 50 rg -n "task-edit-" -S js/desktop/src | head -n 50 ``` Repository: flowy-live/llink Length of output: 14569 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # task-particle-view: inspect state/hooks around editing + useSuspendPlayback sed -n '1,120p' js/desktop/src/features/particles/task-particle-view.tsx sed -n '120,210p' js/desktop/src/features/particles/task-particle-view.tsx # useSuspendPlayback hook implementation sed -n '1,200p' js/desktop/src/hooks/use-suspend-playback.ts # Look for other patterns of tracking open state for “open portals” sed -n '1,120p' js/desktop/src/features/particles/text-reaction-input.tsx sed -n '1,140p' js/desktop/src/features/attachments/attachment-lightbox.tsx ``` Repository: flowy-live/llink Length of output: 14935 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # inspect playback-pause-store to see what happens when suspended ids are active sed -n '1,220p' js/desktop/src/stores/playback-pause-store.ts # inspect where suspension affects playback / global key handlers rg -n "playback-pause-store|usePlaybackPauseStore" js/desktop/src | head -n 50 rg -n "isPaused|paused.*playback|pause.*playback|playbackPause" js/desktop/src | head -n 50 # inspect task-particle-view around editing + blur handling lines nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,210p' # inspect for Radix Select open usage elsewhere in repo (patterns similar to suggested fix) rg -n "onOpenChange={.*set.*Open|onOpenChange" js/desktop/src/features js/desktop/src/components | head -n 50 rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 50 ``` Repository: flowy-live/llink Length of output: 2310 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail sed -n '1,200p' js/desktop/src/stores/playback-pause-store.ts rg -n "usePlaybackPauseStore" js/desktop/src | head -n 100 nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '1,140p' nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,240p' rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 200 rg -n "onOpenChange" js/desktop/src -S | head -n 200 ``` Repository: flowy-live/llink Length of output: 1915 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail sed -n '1,200p' js/desktop/src/stores/playback-pause-store.ts rg -n "usePlaybackPauseStore" js/desktop/src | head -n 100 nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '1,140p' nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,240p' rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 200 rg -n "onOpenChange" js/desktop/src -S | head -n 200 ``` Repository: flowy-live/llink Length of output: 1915 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail sed -n '1,200p' js/desktop/src/stores/playback-pause-store.ts rg -n "usePlaybackPauseStore" js/desktop/src | head -n 100 nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '1,140p' nl -ba js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,240p' rg -n "<Select\\b" js/desktop/src/features js/desktop/src/components -S | head -n 200 rg -n "onOpenChange" js/desktop/src -S | head -n 200 ``` Repository: flowy-live/llink Length of output: 1915 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # task-particle-view: show the exact relevant sections with line numbers cat -n js/desktop/src/features/particles/task-particle-view.tsx | sed -n '140,210p' cat -n js/desktop/src/features/particles/task-particle-view.tsx | sed -n '210,270p' # playback suspension effects: where paused state is checked in key handlers sed -n '1,220p' js/desktop/src/hooks/use-stream-action-keys.ts sed -n '1,220p' js/desktop/src/hooks/use-playback-keys.ts sed -n '240,360p' js/desktop/src/features/particles/stream-view.tsx ``` Repository: flowy-live/llink Length of output: 14647 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail rg -n "\\bpaused\\b" js/desktop/src/features/particles/stream-view.tsx | head -n 80 rg -n "useExitCountdown|useFixedDwell|dwell|playback" js/desktop/src/features/particles/stream-view.tsx | head -n 120 ``` Repository: flowy-live/llink Length of output: 745 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # show editing + suspender wiring in task-particle-view cat -n js/desktop/src/features/particles/task-particle-view.tsx | sed -n '50,110p' # inspect use-fixed-dwell to see how its `paused` prop affects dwell/playback behavior sed -n '1,220p' js/desktop/src/hooks/use-fixed-dwell.ts # confirm select content uses Radix portal + wrapper forwards Root props (for onOpenChange) rg -n "SelectPrimitive\\.Portal|function Select\\(" js/desktop/src/components/ui/select.tsx ``` Repository: flowy-live/llink Length of output: 3413 --- 🌐 Web query: `Radix UI Select onOpenChange focus moved to content portal keyboard focus behavior` 💡 Result: In Radix UI Select, focus behavior when the dropdown opens is mainly controlled by the Select implementation (including its portalized content) rather than directly by `onOpenChange`. When `Select.Content` is rendered in a portal, focus “within” semantics and some focus-related heuristics can differ from non-portal DOM placement, which leads to quirks like “parent loses focus-within” and, in some container setups (e.g., Dialog/Drawer), missing or inconsistent focus on the first item. 1) `onOpenChange` vs focus: what actually moves focus - `onOpenChange` is just the controlled open state callback; Select’s internal focus management happens at open/close time in the Select component code. - Radix’s own Select docs describe keyboard behavior when opening/closing, including that focus is moved to items and returned to the trigger (e.g., Space/Enter opens and focuses the selected/first item; Esc closes and moves focus back to the trigger) [1]. - The Select docs also show that the content “pops out” and is rendered into a portal (defaulting to `document.body`) [1][2]. 2) Why portalization changes focus interactions (the core issue behind the reported behavior) - A known quirk is that ancestors can lose `:focus-within` when Select is opened because `Select.Content` is rendered in a portal (so the content is not within the ancestor’s DOM tree) [3]. This is consistent with browser semantics: `:focus-within` depends on DOM ancestry, not just global focus state. 3) Real-world symptoms with focus not landing on the first item inside overlay containers - There are multiple Radix issues where Select used inside higher-level primitives (Dialog/Drawer) does not focus items as expected on open. - Example: when Select is nested within a Dialog, the first `SelectItem` may not be automatically focused, breaking typeahead/arrow navigation [4]. - Example: inside a Drawer, only the trigger is focusable; opening the Select content does not move focus to the items, so arrow-key navigation doesn’t work [5]. - These reports strongly indicate that “content portal keyboard focus behavior” can be affected by the parent container’s focus management (focus trap, focus restoration, overlay behaviors), even though Select itself intends to focus the appropriate item when opened [1][5]. 4) What this means for your specific query (“focus moved to content portal keyboard focus behavior”) - If by “focus moved to content portal” you mean: “when `Select.Content` is portal-rendered, focus/keyboard interaction does/doesn’t go into the portal content,” then the evidence shows: - Select.Content is portal-rendered by default [1][2]. - Because it’s portal-rendered, DOM-based focus rules like `:focus-within` for ancestors can change [3]. - Additionally, in certain overlay contexts (Dialog/Drawer), Select may fail to focus the first item on open, which is observable as broken keyboard navigation [4][5]. If you tell me which Radix version(s) you’re using and what container you render Select inside (Dialog, Drawer, custom portal, etc.), I can narrow down which focus path is failing and what configuration/workaround to apply (e.g., focusing the trigger vs. ensuring focus trapping doesn’t swallow the initial item focus). Citations: - 1: https://radix-ui-primitives.mintlify.app/components/select - 2: https://www.radix-ui.com/primitives/docs/components/select.md - 3: https://github.com/radix-ui/primitives/issues/1291 - 4: https://github.com/radix-ui/primitives/issues/3306 - 5: https://github.com/radix-ui/primitives/issues/3520 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail rg -n "<Select\\b" js/desktop/src | head -n 80 rg -n "onOpenChange\\s*=\\s*\\{\\s*set.*Open|onOpenChange\\s*=\\s*\\w+Set|onOpenChange\\s*=\\s*set" js/desktop/src/components js/desktop/src/features -S | head -n 80 rg -n "SelectPrimitive\\.Root" js/desktop/src/components/ui/select.tsx ``` Repository: flowy-live/llink Length of output: 1055 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail rg -n "onBlurCapture=\\{\\(e\\) => \\{\\s*if \\(!e\\.currentTarget\\.contains\\(e\\.relatedTarget\\)\\) setEditing\\(false\\);\\s*\\}\\}" -S js/desktop/src || true rg -n "onBlurCapture=.*contains\\(e\\.relatedTarget\\)" -S js/desktop/src | head -n 50 rg -n "useSuspendPlayback\\(.*Select|assigneeMenuOpen|onOpenChange=.*useSuspendPlayback" -S js/desktop/src || true ``` Repository: flowy-live/llink Length of output: 42 --- 🏁 Script executed: ```shell rg -n "onBlurCapture" js/desktop/src | head -n 80 rg -n "relatedTarget" js/desktop/src | head -n 80 ``` Repository: flowy-live/llink Length of output: 291 --- </details> **Suspend playback while the assignee Select menu is open** `task-particle-view.tsx` flips `editing` to `false` on card blur using a DOM containment check; meanwhile the assignee `<Select>` menu is portal-rendered (`SelectPrimitive.Portal`), so focus can move outside the card while the dropdown is open—un-suspending playback and allowing the dwell timer to resume during menu interaction. Track the Select’s open state via `onOpenChange` and include it in the suspender condition. File: js/desktop/src/features/particles/task-particle-view.tsx Lines: 167-170 ## Snippet showing the final state of code at these lines The below code does not show removed or replaced code. ``` onFocusCapture={() => setEditing(true)} onBlurCapture={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setEditing(false); }} ``` ## Comment <details> <summary>Suggested fix</summary> ```diff - const [editing, setEditing] = useState(false); - useSuspendPlayback(editing, `task-edit-${particle.id}`); + const [editing, setEditing] = useState(false); + const [assigneeMenuOpen, setAssigneeMenuOpen] = useState(false); + useSuspendPlayback( + editing || assigneeMenuOpen, + `task-edit-${particle.id}`, + ); … - <Select + <Select + onOpenChange={setAssigneeMenuOpen} value={assigned_to ?? UNASSIGNED} onValueChange={handleAssign} > ``` </details> Also applies to: 222-247 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/task-particle-view.tsx` around lines 167 - 170, The blur handler flips editing off even when the portal-rendered assignee Select is open, so add local state (e.g. isAssigneeOpen) and wire the assignee Select's onOpenChange to set that state, then update the focus/blur suspension logic to also check isAssigneeOpen before calling setEditing(false) (i.e. in onBlurCapture leave editing true if isAssigneeOpen is true); apply the same change to the other similar block that uses setEditing and onBlurCapture so the dropdown interaction doesn't unsuspend playback. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- cr-comment:v1:a2a54d5efe49a3e931d165d6 --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:19 +00:00

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Check whether desktop app mounts under StrictMode (dev double-invocation context).
rg -n -C2 --type=tsx '<StrictMode' js/desktop/src

# 2) Confirm Enter handler currently performs side effects inside updater.
rg -n -C4 --type=ts 'setSelectedIndex\(\(idx\) =>' js/desktop/src/hooks/use-list-keyboard-nav.ts
rg -n -C3 --type=ts 'onOpen\(items\[idx\]\.id\)' js/desktop/src/hooks/use-list-keyboard-nav.ts

Repository: flowy-live/llink

Length of output: 88


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find StrictMode usage (use glob instead of --type=tsx/ts, since rg in this sandbox doesn't recognize those types)
rg -n -C2 '<StrictMode' js/desktop/src -g'*.tsx' -g'*.ts' || true

# 2) Inspect the exact code around the targeted lines in the hook
FILE="js/desktop/src/hooks/use-list-keyboard-nav.ts"
nl -ba "$FILE" | sed -n '50,95p'

# 3) Confirm Enter handler and whether it calls onOpen inside setSelectedIndex updater
rg -n -C4 "if \\(e\\.key === 'Enter'\\)" "$FILE"
rg -n -C3 "setSelectedIndex\\(\\(idx\\) =>" "$FILE"
rg -n -C3 "onOpen\\(items\\[idx\\]\\.id\\)" "$FILE"

Repository: flowy-live/llink

Length of output: 103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find StrictMode usage (sandbox rg may not support --type=tsx/ts)
rg -n -C2 '<StrictMode' js/desktop/src -g'*.tsx' -g'*.ts' || true

# 2) Inspect the targeted code region (use awk since nl isn't available)
FILE="js/desktop/src/hooks/use-list-keyboard-nav.ts"
awk 'NR>=50 && NR<=105 {printf "%d:%s\n", NR, $0}' "$FILE"

# 3) Confirm Enter handler & side effect inside setSelectedIndex updater
rg -n -C4 "if \\(e\\.key === 'Enter'\\)" "$FILE" || true
rg -n -C3 "setSelectedIndex\\(\\(idx\\) =>" "$FILE" || true
rg -n -C3 "onOpen\\(items\\[idx\\]\\.id\\)" "$FILE" || true

Repository: flowy-live/llink

Length of output: 2568


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# A) Check if desktop app uses StrictMode in src (limit output)
rg -n --max-count=20 '<StrictMode' js/desktop/src -g'*.tsx' -g'*.ts' || true

FILE="js/desktop/src/hooks/use-list-keyboard-nav.ts"

# B) Show imports + hook state declarations + keydown useEffect deps
awk 'NR>=1 && NR<=120 {printf "%d:%s\n", NR, $0}' "$FILE"

# C) Show the exact useEffect dependency array line(s) for handleKeyDown registration
rg -n "window\.addEventListener\\('keydown', handleKeyDown" "$FILE" -n
sed -n '90,120p' "$FILE"

Repository: flowy-live/llink

Length of output: 4162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# StrictMode usage anywhere under js/desktop (not just src)
rg -n --max-count=50 '<StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true
rg -n --max-count=50 'React\.StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true

# React version (look for package.json in js/desktop)
fd -t f 'package.json' js/desktop -a | head -n 20
ls -1 js/desktop/package.json 2>/dev/null || true
if [ -f js/desktop/package.json ]; then
  cat js/desktop/package.json | sed -n '1,120p' | rg -n '"react"|react-dom' || true
fi

# Also check for React version in repo root package.json (common in monorepos)
ls -1 package.json 2>/dev/null || true
if [ -f package.json ]; then
  cat package.json | sed -n '1,160p' | rg -n '"react"|react-dom' || true
fi

Repository: flowy-live/llink

Length of output: 223


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --max-count=50 'StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true
rg -n --max-count=50 '<React\.StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true
rg -n --max-count=50 'createRoot' js/desktop -g'*.tsx' -g'*.ts' || true

Repository: flowy-live/llink

Length of output: 890


Remove onOpen side effects from the setSelectedIndex updater (js/desktop/src/hooks/use-list-keyboard-nav.ts, lines ~64-71)

The Enter handler runs onOpen(items[idx].id) inside the functional updater passed to setSelectedIndex. State updater functions should be pure; in concurrent rendering React may invoke them more than once, which can trigger duplicate opens/navigation.

Proposed fix
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
@@
   const [selectedIndex, setSelectedIndex] = useState<number | null>(
     items.length > 0 ? 0 : null,
   );
+  const selectedIndexRef = useRef<number | null>(items.length > 0 ? 0 : null);
+  useEffect(() => {
+    selectedIndexRef.current = selectedIndex;
+  }, [selectedIndex]);
@@
       // Enter: open selected
       if (e.key === 'Enter') {
-        setSelectedIndex((idx) => {
-          if (idx !== null && idx < items.length) {
-            e.preventDefault();
-            onOpen(items[idx].id);
-          }
-          return idx;
-        });
+        const idx = selectedIndexRef.current;
+        if (idx !== null && idx < items.length) {
+          e.preventDefault();
+          onOpen(items[idx].id);
+        }
         return;
       }
📝 Committable suggestion

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

      // Enter: open selected
      if (e.key === 'Enter') {
        const idx = selectedIndexRef.current;
        if (idx !== null && idx < items.length) {
          e.preventDefault();
          onOpen(items[idx].id);
        }
        return;
      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/hooks/use-list-keyboard-nav.ts` around lines 64 - 71, The
Enter key handler currently calls onOpen(items[idx].id) inside the functional
updater passed to setSelectedIndex, which introduces a side effect in a state
updater; change the handler so setSelectedIndex's updater only computes and
returns the new index (no calls to onOpen), capture the current idx in a local
variable or compute it before/after calling setSelectedIndex, then call
onOpen(items[idx].id) outside the updater (ensuring idx is not null and within
items.length). Keep references to setSelectedIndex, onOpen, items and the Enter
key branch when moving the side effect.
_⚠️ Potential issue_ | _🟠 Major_ <details> <summary>🧩 Analysis chain</summary> 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # 1) Check whether desktop app mounts under StrictMode (dev double-invocation context). rg -n -C2 --type=tsx '<StrictMode' js/desktop/src # 2) Confirm Enter handler currently performs side effects inside updater. rg -n -C4 --type=ts 'setSelectedIndex\(\(idx\) =>' js/desktop/src/hooks/use-list-keyboard-nav.ts rg -n -C3 --type=ts 'onOpen\(items\[idx\]\.id\)' js/desktop/src/hooks/use-list-keyboard-nav.ts ``` Repository: flowy-live/llink Length of output: 88 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # 1) Find StrictMode usage (use glob instead of --type=tsx/ts, since rg in this sandbox doesn't recognize those types) rg -n -C2 '<StrictMode' js/desktop/src -g'*.tsx' -g'*.ts' || true # 2) Inspect the exact code around the targeted lines in the hook FILE="js/desktop/src/hooks/use-list-keyboard-nav.ts" nl -ba "$FILE" | sed -n '50,95p' # 3) Confirm Enter handler and whether it calls onOpen inside setSelectedIndex updater rg -n -C4 "if \\(e\\.key === 'Enter'\\)" "$FILE" rg -n -C3 "setSelectedIndex\\(\\(idx\\) =>" "$FILE" rg -n -C3 "onOpen\\(items\\[idx\\]\\.id\\)" "$FILE" ``` Repository: flowy-live/llink Length of output: 103 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # 1) Find StrictMode usage (sandbox rg may not support --type=tsx/ts) rg -n -C2 '<StrictMode' js/desktop/src -g'*.tsx' -g'*.ts' || true # 2) Inspect the targeted code region (use awk since nl isn't available) FILE="js/desktop/src/hooks/use-list-keyboard-nav.ts" awk 'NR>=50 && NR<=105 {printf "%d:%s\n", NR, $0}' "$FILE" # 3) Confirm Enter handler & side effect inside setSelectedIndex updater rg -n -C4 "if \\(e\\.key === 'Enter'\\)" "$FILE" || true rg -n -C3 "setSelectedIndex\\(\\(idx\\) =>" "$FILE" || true rg -n -C3 "onOpen\\(items\\[idx\\]\\.id\\)" "$FILE" || true ``` Repository: flowy-live/llink Length of output: 2568 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # A) Check if desktop app uses StrictMode in src (limit output) rg -n --max-count=20 '<StrictMode' js/desktop/src -g'*.tsx' -g'*.ts' || true FILE="js/desktop/src/hooks/use-list-keyboard-nav.ts" # B) Show imports + hook state declarations + keydown useEffect deps awk 'NR>=1 && NR<=120 {printf "%d:%s\n", NR, $0}' "$FILE" # C) Show the exact useEffect dependency array line(s) for handleKeyDown registration rg -n "window\.addEventListener\\('keydown', handleKeyDown" "$FILE" -n sed -n '90,120p' "$FILE" ``` Repository: flowy-live/llink Length of output: 4162 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # StrictMode usage anywhere under js/desktop (not just src) rg -n --max-count=50 '<StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true rg -n --max-count=50 'React\.StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true # React version (look for package.json in js/desktop) fd -t f 'package.json' js/desktop -a | head -n 20 ls -1 js/desktop/package.json 2>/dev/null || true if [ -f js/desktop/package.json ]; then cat js/desktop/package.json | sed -n '1,120p' | rg -n '"react"|react-dom' || true fi # Also check for React version in repo root package.json (common in monorepos) ls -1 package.json 2>/dev/null || true if [ -f package.json ]; then cat package.json | sed -n '1,160p' | rg -n '"react"|react-dom' || true fi ``` Repository: flowy-live/llink Length of output: 223 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail rg -n --max-count=50 'StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true rg -n --max-count=50 '<React\.StrictMode' js/desktop -g'*.tsx' -g'*.ts' || true rg -n --max-count=50 'createRoot' js/desktop -g'*.tsx' -g'*.ts' || true ``` Repository: flowy-live/llink Length of output: 890 --- </details> **Remove `onOpen` side effects from the `setSelectedIndex` updater** (js/desktop/src/hooks/use-list-keyboard-nav.ts, lines ~64-71) The Enter handler runs `onOpen(items[idx].id)` inside the functional updater passed to `setSelectedIndex`. State updater functions should be pure; in concurrent rendering React may invoke them more than once, which can trigger duplicate opens/navigation. <details> <summary>Proposed fix</summary> ```diff -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; @@ const [selectedIndex, setSelectedIndex] = useState<number | null>( items.length > 0 ? 0 : null, ); + const selectedIndexRef = useRef<number | null>(items.length > 0 ? 0 : null); + useEffect(() => { + selectedIndexRef.current = selectedIndex; + }, [selectedIndex]); @@ // Enter: open selected if (e.key === 'Enter') { - setSelectedIndex((idx) => { - if (idx !== null && idx < items.length) { - e.preventDefault(); - onOpen(items[idx].id); - } - return idx; - }); + const idx = selectedIndexRef.current; + if (idx !== null && idx < items.length) { + e.preventDefault(); + onOpen(items[idx].id); + } return; } ``` </details> <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion // Enter: open selected if (e.key === 'Enter') { const idx = selectedIndexRef.current; if (idx !== null && idx < items.length) { e.preventDefault(); onOpen(items[idx].id); } return; } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/hooks/use-list-keyboard-nav.ts` around lines 64 - 71, The Enter key handler currently calls onOpen(items[idx].id) inside the functional updater passed to setSelectedIndex, which introduces a side effect in a state updater; change the handler so setSelectedIndex's updater only computes and returns the new index (no calls to onOpen), capture the current idx in a local variable or compute it before/after calling setSelectedIndex, then call onOpen(items[idx].id) outside the updater (ensuring idx is not null and within items.length). Keep references to setSelectedIndex, onOpen, items and the Enter key branch when moving the side effect. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:d95db90d27306a2a3a7e18b9 --> <!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +39,4 @@
if (!focused) {
setValue(remoteValue);
}
}
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:19 +00:00

⚠️ Potential issue | 🟠 Major | Quick win

Defer the remote sync instead of consuming it while focused.

Line 38 advances prevRemote even when the field is focused, but Line 40 skips applying that value. If a collaborator changes the field while this input merely has focus, blur won't revisit the sync path and the local UI stays stale until some later remote update arrives. Keep the latest remote value pending while focused, then apply it after focus leaves when there is no local draft to flush.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/hooks/use-live-draft-field.ts` around lines 34 - 42, The
current logic advances prevRemote even while focused, preventing a pending
remote update from being applied on blur; change the sync so you only call
setPrevRemote(remoteValue) when not focused (or when there's no local draft to
flush) and leave prevRemote unchanged while focused so the inequality stays
true; also ensure your blur handler (or the focused state transition in
useLiveDraftField) checks if remoteValue !== prevRemote and, when focus is lost
and there is no local draft to keep, calls setPrevRemote(remoteValue) and
setValue(remoteValue) to apply the pending remote update.
_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Defer the remote sync instead of consuming it while focused.** Line 38 advances `prevRemote` even when the field is focused, but Line 40 skips applying that value. If a collaborator changes the field while this input merely has focus, blur won't revisit the sync path and the local UI stays stale until some later remote update arrives. Keep the latest remote value pending while focused, then apply it after focus leaves when there is no local draft to flush. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/hooks/use-live-draft-field.ts` around lines 34 - 42, The current logic advances prevRemote even while focused, preventing a pending remote update from being applied on blur; change the sync so you only call setPrevRemote(remoteValue) when not focused (or when there's no local draft to flush) and leave prevRemote unchanged while focused so the inequality stays true; also ensure your blur handler (or the focused state transition in useLiveDraftField) checks if remoteValue !== prevRemote and, when focus is lost and there is no local draft to keep, calls setPrevRemote(remoteValue) and setValue(remoteValue) to apply the pending remote update. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- cr-comment:v1:4f69933d7e3c282877b6ee87 --> <!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +34,4 @@
if (prevStreamId !== streamParticle.id) {
setPrevStreamId(streamParticle.id);
setMode(decideMode(streamParticle, userId));
}
coderabbitai[bot] (Migrated from github.com) commented 2026-06-12 18:14:19 +00:00

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React 19 best practice: is calling setState during render (guarded by prop/id comparison) recommended, or should this synchronization be done in useEffect?

💡 Result:

In React 19, calling setState directly during render (the component body) is generally considered an anti-pattern and is discouraged [1][2][3]. The best practice is to derive state during render whenever possible or move the logic to an event handler [1][4][5]. The Core Principles: 1. Prefer Derivation Over State: If a value can be calculated from existing props or state, you should calculate it directly during rendering instead of storing it in a separate state variable [4][5][6]. This eliminates the need for synchronization entirely, avoids extra re-renders, and prevents bugs caused by state getting out of sync [4][5]. 2. Avoid setState in Render: Calling setState during render is dangerous because it can trigger infinite render loops and degrade performance by causing "cascading" updates (where React must discard the current render to start a new one) [1][2][5]. While you can technically guard setState with a condition (e.g., if (props.id !== prevId)) to prevent loops, this is a last resort and is often a sign that your component's state structure should be re-evaluated [1][5]. 3. Use useEffect as a Last Resort: If you must synchronize state based on props (e.g., resetting state when an ID changes), it is generally better to perform this in an event handler (the moment the change is triggered) rather than in an effect [1][4][5]. useEffect should be reserved for synchronizing with external systems (e.g., browser APIs, subscriptions) rather than synchronizing internal React state [6][7]. Synchronous setState in an effect is also inefficient because it forces an extra render cycle [4][8]. Summary of Recommendations: - If you can calculate the data from props or other state: Perform the calculation directly in the component body [4][5]. - If you need to reset/change state when a prop changes: Move the logic to the event handler where the prop (or its source) is updated [1][5]. - If you must use an effect: Ensure it is truly necessary for external synchronization, not just internal state management [6][7]. By moving logic into event handlers or deriving values during render, you align with React's philosophy of maintaining pure, predictable components [5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant portion of the file around the referenced lines
FILE="js/desktop/src/hooks/use-stream-view-mode.ts"
nl -ba "$FILE" | sed -n '1,120p'

Repository: flowy-live/llink

Length of output: 103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="js/desktop/src/hooks/use-stream-view-mode.ts"

# Print a small window around the reported lines; avoid `nl` (not available in sandbox)
sed -n '1,120p' "$FILE" | sed -n '20,80p'

Repository: flowy-live/llink

Length of output: 927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="js/desktop/src/hooks/use-stream-view-mode.ts"

# Show imports + surrounding helpers
sed -n '1,60p' "$FILE"

Repository: flowy-live/llink

Length of output: 1620


Move stream-change synchronization out of render phase in useStreamViewMode.

js/desktop/src/hooks/use-stream-view-mode.ts updates state inside the component body (setPrevStreamId/setMode in the if block at lines 35-36). This is a React anti-pattern that can trigger warnings/extra render churn. Reset mode in a useEffect keyed to the stream identity instead.

♻️ Suggested refactor
-import { useCallback, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
@@
-  // Re-decide when navigating between streams without an unmount.
-  const [prevStreamId, setPrevStreamId] = useState(streamParticle.id);
-  if (prevStreamId !== streamParticle.id) {
-    setPrevStreamId(streamParticle.id);
-    setMode(decideMode(streamParticle, userId));
-  }
+  useEffect(() => {
+    setMode(decideMode(streamParticle, userId));
+  }, [streamParticle.id, userId]);
📝 Committable suggestion

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

  useEffect(() => {
    setMode(decideMode(streamParticle, userId));
  }, [streamParticle.id, userId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/hooks/use-stream-view-mode.ts` around lines 32 - 37, The state
updates for stream changes are happening during render inside useStreamViewMode
(the if block that calls setPrevStreamId and setMode with
prevStreamId/streamParticle.id), which is an anti-pattern; move that logic into
a useEffect: keep prevStreamId and mode state as-is but remove the in-render if,
and create a useEffect keyed on streamParticle.id (and userId if decideMode
depends on it) that compares prevStreamId to streamParticle.id and calls
setPrevStreamId(streamParticle.id) and setMode(decideMode(streamParticle,
userId)) when the id changes so updates occur after render and avoid extra
render churn and warnings.
_⚠️ Potential issue_ | _🟠 Major_ <details> <summary>🧩 Analysis chain</summary> 🌐 Web query: `React 19 best practice: is calling setState during render (guarded by prop/id comparison) recommended, or should this synchronization be done in useEffect?` 💡 Result: In React 19, calling `setState` directly during render (the component body) is generally considered an anti-pattern and is discouraged [1][2][3]. The best practice is to derive state during render whenever possible or move the logic to an event handler [1][4][5]. The Core Principles: 1. Prefer Derivation Over State: If a value can be calculated from existing props or state, you should calculate it directly during rendering instead of storing it in a separate state variable [4][5][6]. This eliminates the need for synchronization entirely, avoids extra re-renders, and prevents bugs caused by state getting out of sync [4][5]. 2. Avoid `setState` in Render: Calling `setState` during render is dangerous because it can trigger infinite render loops and degrade performance by causing "cascading" updates (where React must discard the current render to start a new one) [1][2][5]. While you can technically guard `setState` with a condition (e.g., `if (props.id !== prevId)`) to prevent loops, this is a last resort and is often a sign that your component's state structure should be re-evaluated [1][5]. 3. Use `useEffect` as a Last Resort: If you must synchronize state based on props (e.g., resetting state when an ID changes), it is generally better to perform this in an event handler (the moment the change is triggered) rather than in an effect [1][4][5]. `useEffect` should be reserved for synchronizing with external systems (e.g., browser APIs, subscriptions) rather than synchronizing internal React state [6][7]. Synchronous `setState` in an effect is also inefficient because it forces an extra render cycle [4][8]. Summary of Recommendations: - If you can calculate the data from props or other state: Perform the calculation directly in the component body [4][5]. - If you need to reset/change state when a prop changes: Move the logic to the event handler where the prop (or its source) is updated [1][5]. - If you must use an effect: Ensure it is truly necessary for external synchronization, not just internal state management [6][7]. By moving logic into event handlers or deriving values during render, you align with React's philosophy of maintaining pure, predictable components [5][3]. Citations: - 1: https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-render - 2: https://knowledgelib.io/software/debugging/react-too-many-rerenders/2026 - 3: https://react.dev/reference/rules/components-and-hooks-must-be-pure - 4: https://uk.react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-effect - 5: https://react.dev/learn/you-might-not-need-an-effect - 6: https://juanchi.dev/en/blog/why-i-stopped-using-useeffect-sync-state-react-19 - 7: https://react.dev/reference/react/useEffect - 8: https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-effect --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail # Show the relevant portion of the file around the referenced lines FILE="js/desktop/src/hooks/use-stream-view-mode.ts" nl -ba "$FILE" | sed -n '1,120p' ``` Repository: flowy-live/llink Length of output: 103 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail FILE="js/desktop/src/hooks/use-stream-view-mode.ts" # Print a small window around the reported lines; avoid `nl` (not available in sandbox) sed -n '1,120p' "$FILE" | sed -n '20,80p' ``` Repository: flowy-live/llink Length of output: 927 --- 🏁 Script executed: ```shell #!/bin/bash set -euo pipefail FILE="js/desktop/src/hooks/use-stream-view-mode.ts" # Show imports + surrounding helpers sed -n '1,60p' "$FILE" ``` Repository: flowy-live/llink Length of output: 1620 --- </details> **Move stream-change synchronization out of render phase in `useStreamViewMode`.** `js/desktop/src/hooks/use-stream-view-mode.ts` updates state inside the component body (`setPrevStreamId`/`setMode` in the `if` block at lines 35-36). This is a React anti-pattern that can trigger warnings/extra render churn. Reset `mode` in a `useEffect` keyed to the stream identity instead. <details> <summary>♻️ Suggested refactor</summary> ```diff -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; @@ - // Re-decide when navigating between streams without an unmount. - const [prevStreamId, setPrevStreamId] = useState(streamParticle.id); - if (prevStreamId !== streamParticle.id) { - setPrevStreamId(streamParticle.id); - setMode(decideMode(streamParticle, userId)); - } + useEffect(() => { + setMode(decideMode(streamParticle, userId)); + }, [streamParticle.id, userId]); ``` </details> <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion useEffect(() => { setMode(decideMode(streamParticle, userId)); }, [streamParticle.id, userId]); ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/hooks/use-stream-view-mode.ts` around lines 32 - 37, The state updates for stream changes are happening during render inside useStreamViewMode (the if block that calls setPrevStreamId and setMode with prevStreamId/streamParticle.id), which is an anti-pattern; move that logic into a useEffect: keep prevStreamId and mode state as-is but remove the in-render if, and create a useEffect keyed on streamParticle.id (and userId if decideMode depends on it) that compares prevStreamId to streamParticle.id and calls setPrevStreamId(streamParticle.id) and setMode(decideMode(streamParticle, userId)) when the id changes so updates occur after render and avoid extra render churn and warnings. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:e50fec881ef48a0fe8d87141 --> <!-- This is an auto-generated comment by CodeRabbit -->
Sign in to join this conversation.