feat: stream list view and tasks #279
Reference in New Issue
Block a user
Delete Branch "ecosystem-prototype"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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