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 -->
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.
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.
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.
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.
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.).
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 -->
[](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
Copy Link
Copy Source
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
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.
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.
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.
**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 -->
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 -->
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.
‼️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.
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 -->
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.
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.
_⚠️ 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
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.
‼️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.
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.
_⚠️ 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
#!/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).
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.
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 -->
#!/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 hookFILE="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'||trueFILE="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 ||trueif[ -f js/desktop/package.json ];then
cat js/desktop/package.json | sed -n '1,120p'| rg -n '"react"|react-dom'||truefi# Also check for React version in repo root package.json (common in monorepos)
ls -1 package.json 2>/dev/null ||trueif[ -f package.json ];then
cat package.json | sed -n '1,160p'| rg -n '"react"|react-dom'||truefi
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.
‼️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 -->
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 -->
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].
#!/bin/bash
set -euo pipefail
# Show the relevant portion of the file around the referenced linesFILE="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.
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 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary by CodeRabbit
New Features
Bug Fixes
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
224b64f86band044f1a7457.📒 Files selected for processing (7)
js/desktop/src/features/particles/delete-particle-overlay.tsxjs/desktop/src/features/particles/rename-stream-overlay.tsxjs/desktop/src/features/particles/stream-list-sidebar.tsxjs/desktop/src/features/particles/stream-members-overlay.tsxjs/desktop/src/features/particles/stream-top-bar.tsxjs/desktop/src/features/particles/stream-view.tsxjs/desktop/src/features/particles/task-particle-view.tsx🚧 Files skipped from review as they are similar to previous changes (2)
📝 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
js/desktop/src/api/types.tsChecklistItemSchema,TaskPropertiesSchema, andUnknownParticleSchemawith inferred types. UpdatesParticlePropertiesMapto swapquestfortask. Extendsstreamvariant with optional `status: 'open'js/desktop/src/stores/compose-intent-store.tsComposeIntenttype with new'task'variant and documents it in the intent-step table.js/desktop/src/components/ui/textarea.tsxTextareacomponent that wraps native textarea with Tailwind styling, merges className, and forwards props.js/desktop/src/features/compose/task-compose-step.tsxTaskComposeStepwith local state for title/notes/assignedTo, derives assignees from network, shapesTaskPropertiespayload with conditional fields, and supports keyboard submit/cancel.js/desktop/src/features/compose/compose-overlay.tsxComposeStepunion withtask/configuring/submitting. AddspendingArtifactReffor task storage,handleTaskIntent/handleArtifactSubmithandlers, task keyboard shortcut (D), and rendersTaskComposeStep. Removesdisabledprop gating and usesConfigureContainerStep.js/desktop/src/features/compose/configure-container-step.tsxConfigureStreamSteptoConfigureContainerStep, adds `kind?: 'stream'js/desktop/src/hooks/use-fixed-dwell.tsuseFixedDwellfor elapsed/progress tracking with id resets, pause support, and optionalonProgress/onEndedcallbacks.js/desktop/src/hooks/use-live-draft-field.tsuseLiveDraftFieldfor debounced write-through drafting, focus-aware remote value sync, and flush-on-blur/unmount behavior.js/desktop/src/features/particles/task-particle-view.tsxTaskParticleViewwith live draft editing for title/notes, whole-array checklist writes, assignee updates viaset()/deleteField(), dwell integration, and checklist/assignee UI subcomponents.js/desktop/src/features/particles/text-particle-view.tsxTextParticleViewto delegate dwell/progress/ending touseFixedDwellinstead of local interval management.js/desktop/src/lib/particle-display.tsgetParticleTypeIcon,getMessagePreview(with media/transcript handling), andgetParticleDisplayName(with text truncation).js/desktop/src/features/particles/particle-preview.tsx,js/desktop/src/features/particles/fallback-particle-view.tsxQuestPreviewwithTaskPreview(title + done state styling). Updates fallback to showparticle.raw_typefor unknowns and fixed "Not supported" message.js/desktop/src/hooks/use-container-children.tsuseContainerChildrento query container (stream/folder) and leaf particle children with optional stream-status filtering, visibility scopes, merge/sort by activity, and pagination.js/desktop/src/hooks/use-create-particle.tsCreateParticleParamswith optionalvisibleToandCreateStreamParticleParamswith optionalparentPath, enabling nested streams and visibility control.js/desktop/src/hooks/use-list-keyboard-nav.tsuseStreamKeyboardNavwithuseListKeyboardNavaccepting genericitemsarray andonOpencallback for item activation.js/desktop/src/features/particles/container-view.tsxContainerViewrendering status-filtered children list, keyboard shortcuts (Nfor stream/folder creation), folder/stream creation viaConfigureContainerStep, andContainerControlssubcomponent for hints/actions.js/desktop/src/features/particles/particle-children-list.tsxParticleListViewwithParticleChildrenListsupporting mixed particle types,onOpencallback, empty state, per-type row rendering (StreamRow/FolderRow/LeafRow), and keyboard shortcuts.js/desktop/src/features/particles/folder-view.tsx,js/desktop/src/features/network-root.tsxFolderViewandNetworkRootto delegate rendering toContainerViewwith narrowed/resolved paths.js/desktop/src/features/particles/particle-view-resolver.tsxLayout. IntroducesLeafParticleViewto detect deleted particles and route leaf types (task/media/text/paper) to appropriate viewers.js/desktop/src/hooks/use-stream-view-mode.tsuseStreamViewModeto compute initialmode('player'or'list') from playback markers vs. stream activity and exposetoggle()callback.js/desktop/src/hooks/use-playback-keys.ts,js/desktop/src/hooks/use-stream-navigation-keys.tsusePlaybackKeysto exposespacePausedandresume(). AddsonToggleViewModecallback andLkey handling touseStreamNavigationKeys.js/desktop/src/hooks/use-stream-playback.tsautoAdvanceOnNewoption (defaulttrue) to control whether newly created particles auto-advance playback.js/desktop/src/features/particles/stream-list-sidebar.tsx,js/desktop/src/features/particles/stream-bottom-bar.tsxStreamListSidebarshowing scrollable particle list with chat-row selection,BottomBarwith playback indicator and exit countdown, andStreamViewControlswith compose intent shortcuts (Dfor task, etc.).js/desktop/src/features/particles/stream-view.tsxStreamViewto supportplayer/listmodes, mode-dependent auto-advance/chaining, conditional exit countdown, sidebar rendering, paused resume button, and keyboard mode toggle (L).js/desktop/src/lib/firestore-particles.tssafeDatawrapper, timestamp coercion for leaf properties, unknown-type fallback decoding, resilient read/subscription paths filtering nulls, andbumpParentLastChildCreatedAtto maintain activity ordering on creates.js/mobile/src/lib/firestore-particles.tssafeDatahelper and applies it to all particle read/subscription paths to filter out unparseable documents.Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Poem
✨ Finishing Touches
📝 Generate docstrings
🧪 Generate unit tests (beta)
ecosystem-prototypeActionable comments posted: 8
🧹 Nitpick comments (4)
🤖 Prompt for all review comments with AI agents
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
ℹ️ 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
a358774106and224b64f86b.📒 Files selected for processing (34)
js/desktop/src/api/types.tsjs/desktop/src/components/ui/textarea.tsxjs/desktop/src/features/compose/compose-overlay.tsxjs/desktop/src/features/compose/configure-container-step.tsxjs/desktop/src/features/compose/task-compose-step.tsxjs/desktop/src/features/layout.tsxjs/desktop/src/features/network-root.tsxjs/desktop/src/features/particles/container-view.tsxjs/desktop/src/features/particles/fallback-particle-view.tsxjs/desktop/src/features/particles/folder-view.tsxjs/desktop/src/features/particles/particle-children-list.tsxjs/desktop/src/features/particles/particle-preview.tsxjs/desktop/src/features/particles/particle-view-resolver.tsxjs/desktop/src/features/particles/stream-bottom-bar.tsxjs/desktop/src/features/particles/stream-context-menu.tsxjs/desktop/src/features/particles/stream-list-sidebar.tsxjs/desktop/src/features/particles/stream-top-bar.tsxjs/desktop/src/features/particles/stream-view.tsxjs/desktop/src/features/particles/task-particle-view.tsxjs/desktop/src/features/particles/text-particle-view.tsxjs/desktop/src/hooks/use-container-children.tsjs/desktop/src/hooks/use-create-particle.tsjs/desktop/src/hooks/use-fixed-dwell.tsjs/desktop/src/hooks/use-list-keyboard-nav.tsjs/desktop/src/hooks/use-live-draft-field.tsjs/desktop/src/hooks/use-playback-keys.tsjs/desktop/src/hooks/use-stream-navigation-keys.tsjs/desktop/src/hooks/use-stream-particles.tsjs/desktop/src/hooks/use-stream-playback.tsjs/desktop/src/hooks/use-stream-view-mode.tsjs/desktop/src/lib/firestore-particles.tsjs/desktop/src/lib/particle-display.tsjs/desktop/src/stores/compose-intent-store.tsjs/mobile/src/lib/firestore-particles.ts💤 Files with no reviewable changes (1)
@@ -0,0 +127,4 @@visibleTo,});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.Proposed fix
🤖 Prompt for AI Agents
@@ -0,0 +106,4 @@// Enter only — Space is reserved for play/pause in the stream view.onKeyDown={(e) => {if (e.key === 'Enter') onClick();}}⚠️ 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 inuse-playback-keys.ts(Line 56), keyboard row selection can also toggle playback pause.🐛 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
Use the resolved stream path when updating stream status.
docPathis rebuilt asparticlePath(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 streamParticlePathintoTopBarand calltoFirestoreDocPath(streamPath)directly.Suggested direction
🤖 Prompt for AI Agents
✅ Addressed in commit
044f1a7@@ -0,0 +148,4 @@void writeChecklist([...checklistRef.current, { text, done: false }]);},[writeChecklist],);⚠️ Potential issue | 🟠 Major | ⚡ Quick win
Make
writeChecklistadvance 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. BecauseupdateParticleProperties()writes the entireproperties.checklistfield, 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
📝 Committable suggestion
🤖 Prompt for AI Agents
✅ Addressed in commit
044f1a7@@ -0,0 +171,4 @@onFocusCapture={() => setEditing(true)}onBlurCapture={(e) => {if (!e.currentTarget.contains(e.relatedTarget)) setEditing(false);}}⚠️ Potential issue | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 14569
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 14935
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 2310
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 1915
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 1915
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 1915
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 14647
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 745
🏁 Script executed:
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. WhenSelect.Contentis 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)onOpenChangevs focus: what actually moves focus -onOpenChangeis 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 todocument.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-withinwhen Select is opened becauseSelect.Contentis rendered in a portal (so the content is not within the ancestor’s DOM tree) [3]. This is consistent with browser semantics::focus-withindepends 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 firstSelectItemmay 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: “whenSelect.Contentis 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-withinfor 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:
Repository: flowy-live/llink
Length of output: 1055
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 42
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 291
Suspend playback while the assignee Select menu is open
task-particle-view.tsxflipseditingtofalseon 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 viaonOpenChangeand 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.
Comment
Suggested fix
Also applies to: 222-247
🤖 Prompt for AI Agents
⚠️ Potential issue | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 88
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 103
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 2568
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 4162
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 223
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 890
Remove
onOpenside effects from thesetSelectedIndexupdater (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 tosetSelectedIndex. State updater functions should be pure; in concurrent rendering React may invoke them more than once, which can trigger duplicate opens/navigation.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents
@@ -0,0 +39,4 @@if (!focused) {setValue(remoteValue);}}⚠️ Potential issue | 🟠 Major | ⚡ Quick win
Defer the remote sync instead of consuming it while focused.
Line 38 advances
prevRemoteeven 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
@@ -0,0 +34,4 @@if (prevStreamId !== streamParticle.id) {setPrevStreamId(streamParticle.id);setMode(decideMode(streamParticle, userId));}⚠️ 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
setStatedirectly 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. AvoidsetStatein Render: CallingsetStateduring 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 guardsetStatewith 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. UseuseEffectas 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].useEffectshould be reserved for synchronizing with external systems (e.g., browser APIs, subscriptions) rather than synchronizing internal React state [6][7]. SynchronoussetStatein 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:
Repository: flowy-live/llink
Length of output: 103
🏁 Script executed:
Repository: flowy-live/llink
Length of output: 927
🏁 Script executed:
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.tsupdates state inside the component body (setPrevStreamId/setModein theifblock at lines 35-36). This is a React anti-pattern that can trigger warnings/extra render churn. Resetmodein auseEffectkeyed to the stream identity instead.♻️ Suggested refactor
📝 Committable suggestion
🤖 Prompt for AI Agents