Skip to content

feat: add species lists with fetch, custom lists, and detection filtering#6

Merged
tphakala merged 4 commits intomainfrom
feat/species-lists
Feb 8, 2026
Merged

feat: add species lists with fetch, custom lists, and detection filtering#6
tphakala merged 4 commits intomainfrom
feat/species-lists

Conversation

@tphakala
Copy link
Copy Markdown
Owner

@tphakala tphakala commented Feb 8, 2026

Summary

  • Add a Species tab for managing bird species range lists from eBird data via birda CLI
  • Support fetching species for a location + week (birda species command) and saving as named lists
  • Support creating custom species lists by searching the label service for species names
  • Add species list filter dropdown on the Detections page to filter detections by list membership
  • Fix CoordinateInput map picker rendering in nested modals (portal to body)

New files

File Purpose
src/main/birda/species.ts birda CLI species command wrapper
src/main/db/species-lists.ts Database CRUD for species lists
src/main/ipc/species.ts IPC handlers for species operations
src/renderer/src/pages/SpeciesPage.svelte Species page UI

Modified files

File Change
shared/types.ts New interfaces (SpeciesList, SpeciesListEntry, etc.)
src/main/db/database.ts Migration 2: species_lists + species_list_entries tables
src/main/db/detections.ts species_list_id filter via subquery
src/main/ipc/handlers.ts Register species handlers
src/preload/index.ts Whitelist 6 species:* IPC channels
src/renderer/.../ipc.ts Typed IPC wrappers
src/renderer/.../app.svelte.ts Tab type + selectedSpeciesListId state
src/renderer/.../Sidebar.svelte Species tab with Bird icon
src/renderer/.../App.svelte Route for species tab
src/renderer/.../DetectionsPage.svelte Species list filter dropdown + cross-tab intent
src/renderer/.../CoordinateInput.svelte Portal action for nested modal fix
src/main/index.ts View menu: Species entry (Ctrl+3)
messages/en.json ~35 new i18n keys

Test plan

  • Species tab appears in sidebar between Map and Settings
  • Fetch: enter coordinates + week, click Fetch, verify species list loads
  • Save: name and save a fetched list, verify it appears in the left panel
  • Custom: search species, select several, create list
  • View: click a saved list, verify species table with frequency bars
  • Delete: delete a list, verify removal
  • Detection filter: go to Detections, select species list from dropdown, verify filtering
  • "Use as Detection Filter" button switches to Detections with filter applied
  • Map picker works correctly inside the fetch modal (portal fix)
  • Persistence: restart app, verify lists survive

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a dedicated Species tab with sidebar entry and keyboard shortcut.
    • New Species management UI: view, create, delete, and select species lists.
    • Fetch species by location, week, and threshold with preview and save.
    • Create custom species lists via search and manual selection.
    • In-list search, species detail view, and frequency visualization.
    • Apply a species list as a filter for detections.

…ring

Add a Species tab that lets users fetch expected bird species for a
location and week from birda CLI (eBird data), create custom species
lists from the label service, and filter detections by species list.

- New database tables: species_lists, species_list_entries (migration 2)
- birda CLI species command wrapper using buffered JSON
- Species page with two-panel layout, fetch modal, custom list modal
- Species list dropdown filter on DetectionsPage
- Portal fix for CoordinateInput map picker in nested modals
- Use $state.snapshot() to avoid Proxy serialization over IPC

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 8, 2026

Warning

Rate limit exceeded

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

Walkthrough

Adds a species-list management feature: translations, DB migrations and CRUD, birda CLI integration, IPC handlers, renderer IPC wrappers, sidebar/tab, a SpeciesPage UI, and detection filtering by species list.

Changes

Cohort / File(s) Summary
Translations & Types
messages/en.json, shared/types.ts
Added sidebar_species and ~50 new species.* locale keys. Introduced types: SpeciesList, SpeciesListEntry, EnrichedSpeciesListEntry, SpeciesFetchRequest, BirdaSpeciesResponse; added species_list_id? to DetectionFilter.
Database Migration & DB APIs
src/main/db/database.ts, src/main/db/species-lists.ts, src/main/db/detections.ts
Added Migration 2 creating species_lists and species_list_entries. Implemented species-lists CRUD (create, get, entries, delete, createCustom) with transactions. Extended detections query to support species_list_id filtering via scientific_name IN (species_list_entries).
Birda CLI Integration
src/main/birda/species.ts
New fetchSpecies(latitude, longitude, week, threshold?) that locates birda, executes it with args, parses/validates JSON envelope, and surfaces errors clearly.
Main IPC & Process Wiring
src/main/ipc/species.ts, src/main/ipc/handlers.ts, src/preload/index.ts, src/main/index.ts
Registered species IPC handlers (species:fetch, species:save-list, species:create-custom-list, species:get-lists, species:get-entries, species:delete-list) and invoked registration. Exposed channels in preload. Added Species menu/tab shortcut and adjusted menu accelerators.
Renderer IPC & State
src/renderer/src/lib/utils/ipc.ts, src/renderer/src/lib/stores/app.svelte.ts
Added renderer IPC wrappers (fetch/save/create/get/delete lists, search/resolve labels). Expanded Tab type to include 'species' and added selectedSpeciesListId state.
UI Integration & Components
src/renderer/src/lib/components/Sidebar.svelte, src/renderer/src/lib/components/CoordinateInput.svelte, src/renderer/src/App.svelte
Added Species sidebar tab with Bird icon, integrated SpeciesPage into app routing, and added a portal action to CoordinateInput modal to render at document body.
Species Management UI
src/renderer/src/pages/SpeciesPage.svelte
New comprehensive SpeciesPage (lists panel, detail panel, fetch modal, custom list modal, entry table, in-list search, apply-as-filter action) with full IPC-backed flows and state/UX handling (loading, errors, selection).
Detections Page Integration
src/renderer/src/pages/DetectionsPage.svelte
Loaded available species lists, added a species-list filter dropdown, threaded species_list_id through detection loading, and synchronized selection across tabs via appState.selectedSpeciesListId.

Sequence Diagrams

sequenceDiagram
    actor User
    participant Renderer as SpeciesPage (Renderer)
    participant IPC as Main (IPC)
    participant Birda as Birda CLI
    participant DB as Database

    User->>Renderer: Open Fetch Modal & submit (lat, lon, week, threshold)
    Renderer->>IPC: invoke species:fetch (lat, lon, week, threshold)
    IPC->>Birda: exec birda --lat --lon --week [--threshold]
    Birda-->>IPC: JSON envelope with payload.species
    IPC-->>Renderer: BirdaSpeciesResponse
    User->>Renderer: Save fetched list (name)
    Renderer->>IPC: invoke species:save-list (name, response)
    IPC->>DB: INSERT INTO species_lists
    IPC->>DB: INSERT INTO species_list_entries (transaction)
    DB-->>IPC: created SpeciesList
    IPC-->>Renderer: SpeciesList
Loading
sequenceDiagram
    actor User
    participant Renderer as DetectionsPage (Renderer)
    participant IPC as Main (IPC)
    participant DB as Database

    User->>Renderer: Select species list in dropdown
    Renderer->>IPC: getDetections(filter with species_list_id)
    IPC->>DB: SELECT ... WHERE scientific_name IN (SELECT scientific_name FROM species_list_entries WHERE list_id = ?)
    DB-->>IPC: filtered detections
    IPC-->>Renderer: detections array
    Renderer->>Renderer: update table
Loading
sequenceDiagram
    actor User
    participant Renderer as SpeciesPage (Renderer)
    participant IPC as Main (IPC)
    participant Labels as Labels Service
    participant DB as Database

    User->>Renderer: Open Custom Modal, enter search term
    Renderer->>IPC: labels:search-by-common-name (query)
    IPC->>Labels: search
    Labels-->>IPC: matching scientific names
    IPC-->>Renderer: results
    User->>Renderer: select species and create list (name)
    Renderer->>IPC: species:create-custom-list (name, scientificNames, description)
    IPC->>DB: INSERT INTO species_lists
    IPC->>DB: INSERT INTO species_list_entries
    DB-->>IPC: created SpeciesList
    IPC-->>Renderer: SpeciesList
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main feature additions: species lists with fetch and custom list creation, plus detection filtering functionality.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/species-lists

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

❤️ Share

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

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @tphakala, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's bird species management capabilities by introducing a new 'Species' section. Users can now generate species lists based on geographical location and time of year, create personalized custom lists, and seamlessly apply these lists to filter their detection data. This feature set provides a powerful tool for ornithological data analysis and organization, improving the overall utility for bird enthusiasts and researchers.

Highlights

  • New 'Species' Tab: Introduced a dedicated 'Species' tab for managing bird species lists within the application.
  • eBird Data Integration: Enabled fetching bird species lists for specific geographical locations and weeks, leveraging eBird data via the 'birda' command-line interface.
  • Custom Species List Creation: Provided functionality for users to create and manage their own custom species lists by searching for and selecting species names.
  • Detection Filtering by Species List: Implemented a new filter on the Detections page, allowing users to filter detections based on their membership in a selected species list.
  • Modal Rendering Fix: Resolved a rendering issue with the CoordinateInput map picker when used within nested modals by implementing a Svelte portal action.
Changelog
  • messages/en.json
    • Added numerous internationalization keys to support the new species list features, including titles, button labels, and descriptive texts for fetching and custom list creation.
  • shared/types.ts
    • Introduced new TypeScript interfaces (SpeciesList, SpeciesListEntry, BirdaSpeciesResponse, SpeciesFetchRequest) to define the data structures for species lists and their entries.
    • Extended DetectionFilter to include species_list_id for filtering detections based on species list membership.
  • src/main/birda/species.ts
    • New file implementing the fetchSpecies function, which acts as a wrapper to execute the birda CLI tool for retrieving species data based on location, week, and frequency threshold.
  • src/main/db/database.ts
    • Added a new database migration (Migration 2) to create species_lists and species_list_entries tables.
    • Updated the clearDatabase function to include deletion of these new species-related tables.
  • src/main/db/detections.ts
    • Modified the getDetections function to incorporate species_list_id into the SQL query, enabling filtering of detections by scientific name present in a selected species list.
  • src/main/db/species-lists.ts
    • New file providing core database interaction functions for species lists, including creation of fetched and custom lists, retrieval of lists and their entries, and deletion of lists.
  • src/main/index.ts
    • Updated the application's main process to add a 'Species' entry to the view menu with a Ctrl+3 accelerator, shifting subsequent menu item accelerators.
  • src/main/ipc/handlers.ts
    • Registered the new species IPC handlers, making species-related backend functionality accessible from the renderer process.
  • src/main/ipc/species.ts
    • New file defining the IPC handlers for all species-related operations, such as fetching species from the birda CLI, saving lists to the database, creating custom lists, and retrieving/deleting lists and entries.
    • Includes logic to enrich species entries with resolved common names using the label service.
  • src/preload/index.ts
    • Whitelisted new species:* IPC channels to ensure secure communication between the renderer and main processes for species list management.
  • src/renderer/src/App.svelte
    • Updated the main application component to include routing for the new SpeciesPage, allowing users to navigate to the species management interface.
  • src/renderer/src/lib/components/CoordinateInput.svelte
    • Introduced a portal Svelte action to correctly render the map picker modal, preventing z-index and stacking context issues when nested within other modals.
  • src/renderer/src/lib/components/Sidebar.svelte
    • Added a new 'Species' tab with a bird icon to the application's sidebar navigation.
  • src/renderer/src/lib/stores/app.svelte.ts
    • Extended the Tab type to include 'species'.
    • Added a new state variable selectedSpeciesListId to manage the currently selected species list across the application.
  • src/renderer/src/lib/utils/ipc.ts
    • Added new utility functions for the renderer process to interact with the main process's species IPC handlers, providing typed wrappers for fetching, saving, creating, getting, and deleting species lists.
  • src/renderer/src/pages/DetectionsPage.svelte
    • Integrated a species list filter dropdown into the Detections page, allowing users to filter their detections.
    • Includes logic to react to selectedSpeciesListId changes, enabling cross-tab filtering intent from the Species page.
  • src/renderer/src/pages/SpeciesPage.svelte
    • New file implementing the user interface for the 'Species' tab.
    • Includes displaying existing species lists, modals for fetching new lists (with coordinate input, week, and threshold), and modals for creating custom lists (with species search and selection).
    • Handles list selection, deletion, and applying a list as a detection filter.
Activity
  • The pull request was generated using Claude Code.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant new feature for managing species lists, including fetching from an external source, creating custom lists, and filtering detections. The implementation is comprehensive, covering backend logic, database migrations, IPC communication, and a new UI page. The code is well-structured, particularly the new Svelte components which make good use of Svelte 5 runes for state management. My review focuses on improving error handling and robustness in a few areas, such as replacing unsafe type assertions with safer checks, and ensuring consistency in how IPC calls are made from the renderer. Overall, this is a solid contribution that adds valuable functionality.

Comment on lines +40 to +45
try {
const envelope = JSON.parse(stdout) as BirdaJsonEnvelope;
resolve(envelope.payload as unknown as BirdaSpeciesResponse);
} catch {
reject(new Error(`Failed to parse birda species output: ${stdout.slice(0, 200)}`));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The type assertion as unknown as BirdaSpeciesResponse is unsafe and can lead to runtime errors if the birda CLI payload format changes. A simple property check would make this more robust. Additionally, the catch block for JSON parsing should capture the error object for better diagnostics.

      try {
        const envelope = JSON.parse(stdout) as BirdaJsonEnvelope;
        const payload = envelope.payload;
        if (payload && typeof payload === 'object' && 'species' in payload) {
          resolve(payload as BirdaSpeciesResponse);
        } else {
          reject(new Error('Unexpected payload format from birda species command.'));
        }
      } catch (e) {
        const errorDetails = e instanceof Error ? e.message : String(e);
        reject(new Error(`Failed to parse birda species output: ${errorDetails}. Output: ${stdout.slice(0, 200)}`));
      }

stmt.run(listId, s.scientific_name, s.common_name, s.frequency);
}

return getSpeciesListById(listId)!;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The non-null assertion ! can hide potential bugs. If getSpeciesListById returns undefined for some reason, this will cause a runtime crash. It's safer to explicitly check for the new list and throw a descriptive error if it's not found.

    const newList = getSpeciesListById(listId);
    if (!newList) {
      throw new Error(`Failed to retrieve newly created species list with id ${listId}`);
    }
    return newList;

stmt.run(listId, sn);
}

return getSpeciesListById(listId)!;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The non-null assertion ! can hide potential bugs. If getSpeciesListById returns undefined for some reason, this will cause a runtime crash. It's safer to explicitly check for the new list and throw a descriptive error if it's not found.

    const newList = getSpeciesListById(listId);
    if (!newList) {
      throw new Error(`Failed to retrieve newly created custom species list with id ${listId}`);
    }
    return newList;

Comment on lines +175 to +180
const scientificNames = (await window.birda.invoke(
'labels:search-by-common-name',
customSearchQuery,
)) as string[];
// Resolve common names
const nameMap = (await window.birda.invoke('labels:resolve-all', scientificNames)) as Record<string, string>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

These direct calls to window.birda.invoke are inconsistent with the practice of using typed wrappers from src/renderer/src/lib/utils/ipc.ts. Creating wrappers for these functions would improve type safety and maintainability.

You should add the following wrappers to ipc.ts:

// In src/renderer/src/lib/utils/ipc.ts

export function searchByCommonName(query: string): Promise<string[]> {
  return window.birda.invoke('labels:search-by-common-name', query) as Promise<string[]>;
}

export function resolveAllLabels(scientificNames: string[]): Promise<Record<string, string>> {
  return window.birda.invoke('labels:resolve-all', scientificNames) as Promise<Record<string, string>>;
}

Then, you can import and use them here.

      const scientificNames = await searchByCommonName(customSearchQuery);
      // Resolve common names
      const nameMap = await resolveAllLabels(scientificNames);

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/main/db/species-lists.ts`:
- Around line 52-55: The CI failure is due to unused exported functions: remove
the unnecessary export modifier from getSpeciesListById (it’s used only
internally so make it a plain function) and either delete
getSpeciesListScientificNames entirely or remove its export if you intend to
keep it for future internal use; update the declarations for getSpeciesListById
and getSpeciesListScientificNames accordingly so no unused exports remain.
- Line 43: Replace the forbidden non-null assertion on
getSpeciesListById(listId)! by checking the result at runtime: call const list =
await getSpeciesListById(listId) (or just const list =
getSpeciesListById(listId) if synchronous), if (!list) throw a descriptive Error
(e.g., `Species list not found after insert for id ${listId}`) and then return
list; apply the exact same pattern for the second occurrence at the other call
site (the call that was flagged at line 100) to satisfy the linter and provide a
defensive guard.

In `@src/renderer/src/pages/DetectionsPage.svelte`:
- Around line 161-174: When handling the cross-tab intent in the effect that
watches appState.selectedSpeciesListId (variables: prevSpeciesListIntent,
speciesListFilterId, offset, loadRunDetections), refresh or re-fetch the
speciesLists before applying the intent so the dropdown includes the newly
created list (e.g., call the same loader used in onMount or update speciesLists
if the id is missing), then apply speciesListFilterId, reset offset, and call
loadRunDetections; to allow re-triggering the same-list intent also clear the
intent after consuming it by setting appState.selectedSpeciesListId = null (this
requires a matching change in SpeciesPage.handleUseAsFilter to set the id again
when used).

In `@src/renderer/src/pages/SpeciesPage.svelte`:
- Around line 96-102: The handleUseAsFilter function currently sets
appState.selectedSpeciesListId to selectedList.id, which is a no-op if the value
is already equal and thus won't retrigger the DetectionsPage $effect; modify
handleUseAsFilter to force a change before setting the intended id — for example
set appState.selectedSpeciesListId = null (or decrement/increment a separate
appState.speciesListFilterVersion counter) then assign
appState.selectedSpeciesListId = selectedList.id so the downstream watcher
(prevSpeciesListIntent in DetectionsPage) always observes a change and reloads
the filter when the same list is clicked again.
🧹 Nitpick comments (6)
src/renderer/src/lib/components/CoordinateInput.svelte (1)

27-34: Portal action looks good for the stated purpose.

This is a well-known Svelte pattern for escaping parent stacking contexts. Consider extracting it to a shared utility (e.g., $lib/actions/portal.ts) if other components need the same behavior.

src/main/birda/species.ts (1)

34-47: Consider validating that envelope.payload exists before casting.

If the CLI returns a valid JSON envelope but with a missing or null payload, the cast on line 42 would silently produce an object missing all expected fields, leading to confusing downstream errors.

🛡️ Suggested defensive check
       try {
         const envelope = JSON.parse(stdout) as BirdaJsonEnvelope;
+        if (!envelope.payload) {
+          reject(new Error('birda species response missing payload'));
+          return;
+        }
         resolve(envelope.payload as unknown as BirdaSpeciesResponse);
       } catch {
src/main/db/database.ts (1)

98-99: Redundant delete — CASCADE handles this automatically.

With foreign_keys = ON and ON DELETE CASCADE on species_list_entries, deleting from species_lists would automatically cascade-delete entries. The explicit DELETE FROM species_list_entries is harmless but unnecessary.

src/renderer/src/pages/SpeciesPage.svelte (3)

28-34: Use $derived.by instead of $derived for computed values.

$derived(() => { ... }) produces a derived value that is the arrow function itself, not its return value. On line 391, you call filteredEntries() which works, but Svelte can't track when the derived value changes (the function reference is stable). This means the filter logic reruns on every unrelated template re-render instead of only when entries or entryFilter change.

Proposed fix
-  const filteredEntries = $derived(() => {
+  const filteredEntries = $derived.by(() => {
     if (!entryFilter) return entries;
     const q = entryFilter.toLowerCase();
     return entries.filter(
       (e) => e.resolved_common_name.toLowerCase().includes(q) || e.scientific_name.toLowerCase().includes(q),
     );
   });

Then on line 391, use filteredEntries without the call:

-              {`#each` filteredEntries() as entry (entry.id)}
+              {`#each` filteredEntries as entry (entry.id)}

304-308: Destructive delete action has no confirmation prompt.

handleListDelete immediately deletes the species list without a confirmation dialog. Since lists can contain fetched data that takes time to reproduce, consider adding a confirmation step.


168-188: Add typed wrappers for label search and resolution IPC calls.

The doCustomSearch() function uses raw window.birda.invoke calls for labels:search-by-common-name and labels:resolve-all, while the rest of this file uses typed wrappers from $lib/utils/ipc (e.g., fetchSpeciesList, getSpeciesLists). These two channels lack wrapper functions in ipc.ts, creating an inconsistency in type safety and code style.

Add typed wrappers in $lib/utils/ipc for these two channels to maintain consistency with the rest of the codebase.

tphakala and others added 2 commits February 8, 2026 19:05
- Remove unused exports (getSpeciesListById, getSpeciesListScientificNames)
  to fix knip CI failure
- Replace non-null assertions with runtime checks in species-lists.ts
- Add payload validation in birda species CLI wrapper
- Use $derived.by instead of $derived for filteredEntries in SpeciesPage
- Add typed IPC wrappers (searchByCommonName, resolveAllLabels) in ipc.ts
- Fix cross-tab intent: refresh species lists dropdown and allow
  re-triggering same list via "Use as Detection Filter"
- Add missing i18n keys for hardcoded English strings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The type checker requires casting through unknown when converting
Record<string, unknown> to BirdaSpeciesResponse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/main/birda/species.ts`:
- Around line 40-51: The code currently uses "'species' in payload" which throws
when envelope.payload is undefined; update the parsing logic in the try block
(around envelope and payload variables in this module) to first verify payload
exists and is an object (e.g., if (!payload || typeof payload !== 'object' ||
!('species' in payload)) ) and then reject with a clear "Malformed birda
envelope: missing payload or species" (or similar) error instead of letting the
in-operator throw; ensure you still resolve with payload cast to
BirdaSpeciesResponse when valid and keep the existing catch for genuine JSON
parse errors.

In `@src/renderer/src/pages/SpeciesPage.svelte`:
- Around line 202-217: The handler handleCreateCustomList silently logs errors;
update it to set a user-visible error state (e.g., create a new variable like
customCreateError or reuse an existing fetchError) inside the catch block and
keep showCustomModal true so the modal stays open; ensure the modal
component/template displays that error (inline text or toast) when
customCreateError is set and clear the error on successful creation or when the
modal is closed; reference handleCreateCustomList, createCustomSpeciesList,
showCustomModal, and the modal's error display logic to implement this.
🧹 Nitpick comments (5)
src/main/birda/species.ts (1)

12-32: Consider validating numeric inputs before spawning the process.

If NaN or Infinity leaks in (e.g. from a UI parsing bug), String(NaN) is sent to the CLI, producing a confusing downstream error. A quick guard at the top would surface the problem earlier.

🛡️ Suggested guard
 ): Promise<BirdaSpeciesResponse> {
+  if (!Number.isFinite(latitude) || !Number.isFinite(longitude) || !Number.isFinite(week)) {
+    throw new Error('latitude, longitude, and week must be finite numbers');
+  }
   const birdaPath = await findBirda();
src/renderer/src/pages/SpeciesPage.svelte (3)

170-185: Resolve labels only for the displayed slice, not the entire result set.

resolveAllLabels is called with all results from searchByCommonName (potentially hundreds), but only the first 50 are shown. Slice first, then resolve, to avoid unnecessary IPC overhead.

♻️ Proposed fix
   async function doCustomSearch() {
     if (!customSearchQuery.trim()) {
       customSearchResults = [];
       return;
     }
     try {
       const scientificNames = await searchByCommonName(customSearchQuery);
-      const nameMap = await resolveAllLabels(scientificNames);
-      customSearchResults = scientificNames.slice(0, 50).map((sn) => ({
+      const sliced = scientificNames.slice(0, 50);
+      const nameMap = await resolveAllLabels(sliced);
+      customSearchResults = sliced.map((sn) => ({
         scientific_name: sn,
         common_name: nameMap[sn] ?? sn,
       }));
     } catch {
       customSearchResults = [];
     }
   }

85-96: List deletion has no confirmation prompt — accidental deletes are irrecoverable.

handleListDelete immediately deletes without any user confirmation. A fetched list (which required network + CLI invocation) cannot be easily recreated. Consider adding a simple confirmation step (e.g., window.confirm or a small inline prompt).


98-104: Synchronous null-then-set may be batched in Svelte 5.

In Svelte 5, effects run after the current microtask. Setting selectedSpeciesListId = null then immediately = selectedList.id in the same synchronous call means the effect only observes the final value. This works for the normal flow (because DetectionsPage nullifies after consuming), but won't re-trigger if the user clicks "Use as Filter" very rapidly before the effect runs.

This is an unlikely edge case and was addressed per prior review feedback, so flagging only for awareness.

src/renderer/src/pages/DetectionsPage.svelte (1)

235-250: Species list dropdown is stale if user creates a list without using "Use as Filter".

The dropdown is populated on mount and refreshed only via the cross-tab intent. If the user navigates to Species, creates/deletes lists, then manually navigates back to Detections (without clicking "Use as Filter"), the dropdown will show stale data until the next page mount.

A lightweight fix would be to refresh the list when the detections tab becomes active:

♻️ Sketch: refresh on tab activation

Add to an existing or new $effect:

$effect(() => {
  if (appState.activeTab === 'detections') {
    getSpeciesLists()
      .then((l) => (speciesLists = l))
      .catch(() => {});
  }
});

This could be merged into the existing cross-tab intent effect or kept separate depending on preference.

- Guard against undefined envelope.payload in birda species CLI wrapper
- Add user-visible error feedback for custom species list creation failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tphakala tphakala merged commit 8ff51c5 into main Feb 8, 2026
5 checks passed
@tphakala tphakala deleted the feat/species-lists branch February 8, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant