Skip to content

feat: initialize Emdash extension with core files and API :#1564

Open
yashdev9274 wants to merge 1 commit intogeneralaction:mainfrom
yashdev9274:feat-yd-emdash-raycastEntension
Open

feat: initialize Emdash extension with core files and API :#1564
yashdev9274 wants to merge 1 commit intogeneralaction:mainfrom
yashdev9274:feat-yd-emdash-raycastEntension

Conversation

@yashdev9274
Copy link
Copy Markdown
Contributor

@yashdev9274 yashdev9274 commented Mar 23, 2026

Summary

  • Developed core components for recent projects, search projects, tasks, and SSH connections.
  • Created package.json and package-lock.json for dependency management.
  • Implemented TypeScript configuration in tsconfig.json.
  • Established API service for data handling and local server communication.
  • Included assets such as the application icon.
  • Set up environment type definitions for TypeScript support.

Fixes

new feat- Raycast Emdash extension with commands: search /open projects, recent projects, task, ssh connection

Snapshot

emdash-raycast.mp4

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • This change requires a documentation update

Mandatory Tasks

  • I have self-reviewed the code
  • A decent size PR without self-review might be rejected

Checklist

  • I have read the contributing guide
  • My code follows the style guidelines of this project (pnpm run format)
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my PR needs changes to the documentation
  • I have checked if my changes generate no new warnings (pnpm run lint)
  • I have added tests that prove my fix is effective or that my feature works
  • I haven't checked if new and existing unit tests pass locally with my changes

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Raycast extension with commands to search, browse recent projects, view tasks, and access SSH connections
    • Enabled deep-link support via emdash:// protocol to open projects and tasks from external applications
    • Added local HTTP API for accessing projects, tasks, and SSH connections
    • Implemented automatic data export for integration with external tools
    • Enhanced system tray with improved context menu navigation

- Created package.json and package-lock.json for dependency management.
- Implemented TypeScript configuration in tsconfig.json.
- Developed core components for recent projects, search projects, tasks, and SSH connections.
- Established API service for data handling and local server communication.
- Included assets such as the application icon.
- Set up environment type definitions for TypeScript support.
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 23, 2026

@yashdev9274 is attempting to deploy a commit to the General Action Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a Raycast extension for browsing Emdash projects, tasks, and SSH connections, alongside support for emdash:// deep-linking protocol in the main application. New backend services export project data and expose a local HTTP API. The main process lifecycle, system tray, and renderer integrate deep-link event handling.

Changes

Cohort / File(s) Summary
Raycast Extension Configuration
raycast-emdash/.nvmrc, raycast-emdash/package.json, raycast-emdash/tsconfig.json
Setup files for Raycast extension: pinned Node.js version, project metadata, extension commands (search-projects, recent-projects, tasks, ssh-connections), dependencies, and TypeScript configuration.
Raycast Type Definitions & API
raycast-emdash/raycast-env.d.ts, raycast-emdash/src/lib/types.ts, raycast-emdash/src/lib/api.ts
Type interfaces for Project, Task, SshConnection and a cached filesystem API (EmdashApi) that reads exported Emdash data from ~/Library/Application Support/emdash/emdash-data.json with methods to fetch projects, recent projects, tasks, and SSH connections.
Raycast UI Components
raycast-emdash/src/search-projects.tsx, raycast-emdash/src/recent-projects.tsx, raycast-emdash/src/tasks.tsx, raycast-emdash/src/ssh-connections.tsx
Four Raycast List-based components that fetch and display projects (searchable and recent), tasks, and SSH connections. Each includes actions to open items in Emdash via deep-link protocol or system file manager.
App Lifecycle & System Tray
src/main/app/lifecycle.ts, src/main/app/tray.ts
Main process app lifecycle initialization that creates the main window and system tray on startup. Tray module manages a global tray instance with context menu (open/focus window, quit app) and double-click handler.
Backend Data & API Services
src/main/services/DataExportService.ts, src/main/services/LocalApiService.ts
New services: DataExportService periodically exports projects, tasks, and SSH connections to emdash-data.json; LocalApiService boots an HTTP server on 127.0.0.1:3847 exposing /api/v1 endpoints for projects (with recent filter), tasks (with optional projectId), and SSH connections.
Database & IPC Extensions
src/main/services/DatabaseService.ts, src/main/ipc/dbIpc.ts
Added getProjectByPath() method to query projects by file path and exposed it via IPC endpoint.
Deep-Link Protocol Handler
src/main/main.ts, src/main/preload.ts, src/renderer/types/electron-api.d.ts, src/renderer/views/Workspace.tsx
Protocol registration for emdash:// scheme with URL parsing (project/task query params), main window auto-creation, and IPC event emission to renderer. Added onDeepLink() preload API and renderer hook to handle deep-link events for project/task navigation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Raycast
    participant MainProcess as Main Process<br/>(Protocol Handler)
    participant Renderer as Renderer<br/>(React App)

    User->>Raycast: Click action in Raycast extension
    Raycast->>MainProcess: Open emdash://url?project=path or task=id
    MainProcess->>MainProcess: Parse URL params
    MainProcess->>MainProcess: Ensure main window exists<br/>(create if needed)
    MainProcess->>Renderer: Send IPC deep-link event<br/>(type, project, taskId)
    Renderer->>Renderer: Handle onDeepLink callback<br/>Activate project/task view
    Renderer->>User: Display selected project or task
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Poem

🐰 A Raycast window opens wide,
Deep-link protocol as our guide,
Projects leap, tasks align,
SSH connections shine,
Data flows—Emdash takes a ride! 🎯

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 describes the main change: initializing a Raycast Emdash extension with core files and API infrastructure, which is the primary focus of all the added files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

🧹 Nitpick comments (8)
src/main/services/LocalApiService.ts (2)

123-127: Use getProjectById for project existence check.

Same inefficient pattern here. Since you're validating that a project exists before creating a task, use the direct lookup method.

♻️ Proposed fix
-      const projects = await databaseService.getProjects();
-      const project = projects.find((p) => p.id === projectId);
+      const project = await databaseService.getProjectById(projectId);
       if (!project) {
         return { success: false, error: 'Project not found' };
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/LocalApiService.ts` around lines 123 - 127, Replace the
inefficient getProjects() + find(...) pattern with a direct lookup using
databaseService.getProjectById(projectId): in the block where you currently call
getProjects() and search for project (variables projectId, projects, project),
call getProjectById(projectId) instead and check the returned value for
null/undefined; return the same { success: false, error: 'Project not found' }
if it doesn't exist. Ensure you update any references to the previous projects
array and keep error behavior unchanged.

96-102: Use getProjectById for single project lookup.

Fetching all projects and filtering client-side is inefficient. The DatabaseService already has a getProjectById method that performs a direct lookup.

♻️ Proposed fix
     if (req.method === 'GET' && pathname.startsWith(`${API_BASE}/projects/`)) {
       const id = pathname.split('/').pop();
       if (!id) return { success: false, error: 'Missing project ID' };
-      const projects = await databaseService.getProjects();
-      const project = projects.find((p) => p.id === id);
+      const project = await databaseService.getProjectById(id);
       if (!project) return { success: false, error: 'Project not found' };
       return { success: true, data: project };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/LocalApiService.ts` around lines 96 - 102, The current GET
handler extracts the project id from pathname and calls
databaseService.getProjects() then filters; replace that with a direct lookup
using databaseService.getProjectById(id): extract id as you already do, call
await databaseService.getProjectById(id), check for null/undefined and return {
success: false, error: 'Project not found' } if missing, otherwise return the
found project; remove the getProjects() call and client-side .find usage and
keep the surrounding request check that uses API_BASE and the GET method.
src/renderer/views/Workspace.tsx (1)

254-259: The setTimeout delay for task selection is fragile.

Using a fixed 100ms delay to wait for project activation before selecting a task is a timing-based workaround. While this follows patterns used elsewhere in the codebase (e.g., handleTitlebarKanbanToggle), consider using a callback or effect-based approach for more reliable sequencing in future iterations.

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

In `@src/renderer/views/Workspace.tsx` around lines 254 - 259, The current
setTimeout-based selection (using setTimeout(() => { const entry =
taskMgmt.allTasks.find(...); if (entry) taskMgmt.handleSelectTask(entry.task);
}, 100)) is fragile; replace this timing workaround by sequencing the selection
after project activation completes (e.g., via a callback, returned Promise, or
an effect that watches the activated project) so selection happens
deterministically. Modify the code path that activates the project (the function
that currently precedes this setTimeout) to either accept a completion callback
or return a Promise, then call taskMgmt.handleSelectTask(entry.task) from that
completion handler (or move the selection into an effect that reacts to the
active project change) instead of relying on setTimeout; reference
taskMgmt.allTasks and taskMgmt.handleSelectTask and the similar pattern in
handleTitlebarKanbanToggle when refactoring.
src/main/app/tray.ts (1)

16-18: Production icon path may fail if process.resourcesPath is undefined.

If process.resourcesPath is undefined in certain Electron configurations, the fallback to empty string ('') will produce an invalid path icon.png. The subsequent try/catch handles this gracefully by falling back to an empty icon, but consider logging a warning when this occurs.

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

In `@src/main/app/tray.ts` around lines 16 - 18, The production icon path
construction using iconPath (computed from isDev ? ... :
join(process.resourcesPath || '', 'icon.png')) can produce an invalid path when
process.resourcesPath is undefined; modify the logic that sets iconPath to
detect undefined process.resourcesPath and, if so, log a warning (via the
existing logger or console.warn) before falling back to the current fallback
behavior so you have visibility into the condition; update the code that
consumes iconPath (the existing try/catch fallback to an empty icon) to retain
its behavior but ensure the new warning is emitted when process.resourcesPath is
missing.
src/main/services/DataExportService.ts (2)

39-41: String comparison for change detection may be inefficient for large datasets.

Comparing full JSON strings works correctly but allocates memory for the entire serialized payload twice. For small-to-medium datasets this is fine; for very large datasets, consider a hash-based comparison.

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

In `@src/main/services/DataExportService.ts` around lines 39 - 41, Replace the
full JSON string equality check with a hash-based comparison to avoid allocating
two large serialized payloads: compute a stable/hash-safe digest (e.g., SHA-256
of a canonical/stable serialization) for the payload in the same place where
`json` is produced, store that digest on the instance (e.g.,
`this.lastExportHash`) instead of `this.lastExport`, and change the early-return
check from `if (json === this.lastExport)` to compare the two digests; update
any places that relied on `this.lastExport` to use the stored hash or keep the
payload separately when needed (refer to `json`, `this.lastExport`, and the
DataExportService method containing this check).

46-49: Redundant directory existence check.

app.getPath('userData') returns a path that Electron guarantees to exist (it creates it if needed). The directory check is unnecessary.

♻️ Remove redundant check
       const userDataPath = app.getPath('userData');
       const exportPath = join(userDataPath, EXPORT_FILE);
 
-      const dir = userDataPath;
-      if (!existsSync(dir)) {
-        mkdirSync(dir, { recursive: true });
-      }
-
       writeFileSync(exportPath, json, 'utf-8');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/DataExportService.ts` around lines 46 - 49, The code in
DataExportService.ts redundantly checks for and creates the Electron userData
directory; remove the unnecessary existence check and creation by deleting the
if-block that calls existsSync(dir) and mkdirSync(dir, { recursive: true }),
leaving only the assignment const dir = userDataPath; so the code relies on
Electron to ensure userData exists (references: userDataPath variable, dir,
existsSync, mkdirSync).
raycast-emdash/src/lib/api.ts (2)

40-48: Inconsistent error handling across methods.

getProjects() has try/catch with logging and re-throw, while other methods (getRecentProjects, getTasks, getSshConnections) let exceptions propagate silently. Consider applying consistent error handling or removing the redundant try/catch here since it only logs and re-throws.

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

In `@raycast-emdash/src/lib/api.ts` around lines 40 - 48, getProjects currently
wraps loadData in a try/catch that only logs and re-throws, creating
inconsistent behavior with getRecentProjects, getTasks, and getSshConnections;
remove the try/catch from getProjects so it mirrors the other methods (i.e.,
simply await this.loadData() and return data.projects) to keep error propagation
consistent, or alternatively add equivalent try/catch+logging to the other
methods if you prefer centralized logging—update the implementation of
getProjects (and/or getRecentProjects/getTasks/getSshConnections) accordingly.

21-38: Synchronous file operations in async method may block the event loop.

existsSync, statSync, and readFileSync are blocking operations used inside an async method. For a Raycast extension where the data file is typically small and local, this is unlikely to cause noticeable issues, but switching to async variants (fs.promises) would be more idiomatic.

♻️ Optional: Use async fs operations
-import { existsSync, readFileSync, statSync } from 'fs';
+import { access, readFile, stat } from 'fs/promises';
 import type { Project, Task, SshConnection } from './types';
   private async loadData(): Promise<ExportData> {
     const exportPath = getExportPath();
 
-    if (!existsSync(exportPath)) {
+    try {
+      await access(exportPath);
+    } catch {
       throw new Error('Emdash data not found. Please open Emdash first.');
     }
 
-    const stats = statSync(exportPath);
+    const stats = await stat(exportPath);
     const mtime = stats.mtimeMs;
 
     if (!this.cache || mtime > this.lastModified) {
-      const content = readFileSync(exportPath, 'utf-8');
+      const content = await readFile(exportPath, 'utf-8');
       this.cache = JSON.parse(content);
       this.lastModified = mtime;
     }
 
     return this.cache!;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raycast-emdash/src/lib/api.ts` around lines 21 - 38, The loadData method uses
blocking fs calls (existsSync, statSync, readFileSync) which can block the event
loop; update loadData to use async fs.promises equivalents (e.g.,
fs.promises.stat and fs.promises.readFile or fs.promises.access) while
preserving the same logic: compute exportPath via getExportPath(), await an
async existence check or handle stat rejection, read and JSON.parse the file
only when mtime > this.lastModified or this.cache is undefined, and update
this.cache and this.lastModified accordingly; ensure errors are thrown the same
way and update imports/usages in the module to use fs.promises where needed so
loadData remains async and non-blocking.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@raycast-emdash/.nvmrc`:
- Line 1: Update the Node.js version specified in the .nvmrc from "22.14.0" to
the project's standard "22.20.0" so the repository uses the consistent runtime;
locate the .nvmrc entry containing "22.14.0" and replace it with "22.20.0" (also
check any package.json engines field if present to keep them in sync).

In `@raycast-emdash/src/ssh-connections.tsx`:
- Line 1: The Connect action calls open(...) but the symbol isn't imported;
update the top import statement that currently reads "import { List,
ActionPanel, Action } from '@raycast/api'" to also include open so TypeScript
can resolve it; locate the Connect action (the component/function that invokes
open on line ~31) to confirm usage and then add open to the import list from
'@raycast/api'.

In `@raycast-emdash/tsconfig.json`:
- Around line 15-16: The tsconfig.json currently only includes "src/**/*", so
the ambient declarations in raycast-env.d.ts at the package root aren't picked
up; update the tsconfig "include" to also cover the root declaration (e.g., add
"raycast-env.d.ts" or a root glob like "*.d.ts") so the ambient types from
raycast-env.d.ts are included in the TypeScript program and available to code
referencing Preferences/Arguments.

In `@src/main/app/lifecycle.ts`:
- Around line 6-10: The app.whenReady() block in lifecycle.ts is creating a
second startup window by calling createMainWindow(); remove that duplicate call
and only perform startup tasks that are safe to run here (e.g., createTray()).
Specifically, delete or stop invoking createMainWindow() and
setMainWindowRef(win) from this file so the single-window creation remains in
src/main/main.ts (where createMainWindow() and IPC/database setup occur); ensure
createTray() remains here (or is moved if desired) and that any code relying on
the main window reference uses the setMainWindowRef call only from the main.ts
startup path where the window is actually created.

In `@src/main/app/tray.ts`:
- Around line 40-48: The tray menu click handler doesn't update the local
mainWindow reference when creating the window dynamically; modify the click
branch so it captures and stores the created window: require('./window') to get
createMainWindow and assign its return to the local mainWindow variable (i.e.,
mainWindow = createMainWindow()); ensure createMainWindow returns the new
BrowserWindow instance (update createMainWindow implementation if necessary) so
subsequent handlers (e.g., the tray double-click logic that checks mainWindow)
will operate on the new window.

In `@src/main/main.ts`:
- Around line 329-331: The open-url handler (app.on('open-url', async (event,
url) => { ... })) currently logs the raw url which can leak local filesystem
paths for project= links; parse the incoming url (the url variable) and, if it
contains a project= query value, redact or replace the path portion (e.g., strip
directories and/or replace with "<REDACTED_PATH>" or only keep the repo name)
before calling console.log; update the log call to use the sanitizedUrl so other
URLs still log normally while project= payloads never expose full local paths.
- Around line 335-391: The deep-link parser (parsedUrl / projectPath / taskId)
currently ignores SSH links; add handling for the SSH query parameter (e.g.,
connectionId = parsedUrl.searchParams.get('connection')) alongside project/task
branches. After ensuring win is ready, detect connectionId and call the
appropriate DB accessor (e.g., databaseService.getSshConnectionById or similar),
then send a deep-link via win.webContents.send('deep-link', { type: 'open-ssh',
connection: connection || undefined, connectionId }) so the renderer can open
the SSH connection; if the DB accessor returns null just send the connectionId
so the UI can handle missing entries. Ensure you reference parsedUrl,
databaseService, getSshConnectionById (or the existing SSH lookup method), and
the 'deep-link' IPC channel.
- Around line 327-329: The open-url listener is currently registered inside
app.whenReady(), which can miss cold-start protocol events on macOS; move the
app.on('open-url', ...) registration out of whenReady() (or attach it in
app.on('will-finish-launching')) so it runs early, capture the incoming url into
a module-scoped pendingUrl variable, and then in app.whenReady() call your
existing URL processing logic (e.g., invoke handleOpenUrl(pendingUrl) or the
same handler used by the listener) after the window/UI is created; keep
app.setAsDefaultProtocolClient('emdash') where it is but ensure the listener and
pendingUrl/handleOpenUrl symbols are used to defer handling until ready.

In `@src/main/services/DataExportService.ts`:
- Around line 81-91: The exported task objects in DataExportService (the
tasks.map transform) are missing the archivedAt property, so Raycast receives
tasks with archivedAt undefined; update the mapping inside DataExportService
(the tasks: tasks.map(...) block) to include archivedAt: t.archivedAt so the
exported tasks preserve their archived status (nullable) and Raycast's
!t.archivedAt filter behaves correctly.

In `@src/main/services/LocalApiService.ts`:
- Line 3: Remove the unused BrowserWindow import from LocalApiService.ts: locate
the import line that reads "import { BrowserWindow } from 'electron';" and
delete it (or remove BrowserWindow from the import list) so the file no longer
imports an unused symbol; ensure no other code in LocalApiService references
BrowserWindow after this change.

---

Nitpick comments:
In `@raycast-emdash/src/lib/api.ts`:
- Around line 40-48: getProjects currently wraps loadData in a try/catch that
only logs and re-throws, creating inconsistent behavior with getRecentProjects,
getTasks, and getSshConnections; remove the try/catch from getProjects so it
mirrors the other methods (i.e., simply await this.loadData() and return
data.projects) to keep error propagation consistent, or alternatively add
equivalent try/catch+logging to the other methods if you prefer centralized
logging—update the implementation of getProjects (and/or
getRecentProjects/getTasks/getSshConnections) accordingly.
- Around line 21-38: The loadData method uses blocking fs calls (existsSync,
statSync, readFileSync) which can block the event loop; update loadData to use
async fs.promises equivalents (e.g., fs.promises.stat and fs.promises.readFile
or fs.promises.access) while preserving the same logic: compute exportPath via
getExportPath(), await an async existence check or handle stat rejection, read
and JSON.parse the file only when mtime > this.lastModified or this.cache is
undefined, and update this.cache and this.lastModified accordingly; ensure
errors are thrown the same way and update imports/usages in the module to use
fs.promises where needed so loadData remains async and non-blocking.

In `@src/main/app/tray.ts`:
- Around line 16-18: The production icon path construction using iconPath
(computed from isDev ? ... : join(process.resourcesPath || '', 'icon.png')) can
produce an invalid path when process.resourcesPath is undefined; modify the
logic that sets iconPath to detect undefined process.resourcesPath and, if so,
log a warning (via the existing logger or console.warn) before falling back to
the current fallback behavior so you have visibility into the condition; update
the code that consumes iconPath (the existing try/catch fallback to an empty
icon) to retain its behavior but ensure the new warning is emitted when
process.resourcesPath is missing.

In `@src/main/services/DataExportService.ts`:
- Around line 39-41: Replace the full JSON string equality check with a
hash-based comparison to avoid allocating two large serialized payloads: compute
a stable/hash-safe digest (e.g., SHA-256 of a canonical/stable serialization)
for the payload in the same place where `json` is produced, store that digest on
the instance (e.g., `this.lastExportHash`) instead of `this.lastExport`, and
change the early-return check from `if (json === this.lastExport)` to compare
the two digests; update any places that relied on `this.lastExport` to use the
stored hash or keep the payload separately when needed (refer to `json`,
`this.lastExport`, and the DataExportService method containing this check).
- Around line 46-49: The code in DataExportService.ts redundantly checks for and
creates the Electron userData directory; remove the unnecessary existence check
and creation by deleting the if-block that calls existsSync(dir) and
mkdirSync(dir, { recursive: true }), leaving only the assignment const dir =
userDataPath; so the code relies on Electron to ensure userData exists
(references: userDataPath variable, dir, existsSync, mkdirSync).

In `@src/main/services/LocalApiService.ts`:
- Around line 123-127: Replace the inefficient getProjects() + find(...) pattern
with a direct lookup using databaseService.getProjectById(projectId): in the
block where you currently call getProjects() and search for project (variables
projectId, projects, project), call getProjectById(projectId) instead and check
the returned value for null/undefined; return the same { success: false, error:
'Project not found' } if it doesn't exist. Ensure you update any references to
the previous projects array and keep error behavior unchanged.
- Around line 96-102: The current GET handler extracts the project id from
pathname and calls databaseService.getProjects() then filters; replace that with
a direct lookup using databaseService.getProjectById(id): extract id as you
already do, call await databaseService.getProjectById(id), check for
null/undefined and return { success: false, error: 'Project not found' } if
missing, otherwise return the found project; remove the getProjects() call and
client-side .find usage and keep the surrounding request check that uses
API_BASE and the GET method.

In `@src/renderer/views/Workspace.tsx`:
- Around line 254-259: The current setTimeout-based selection (using
setTimeout(() => { const entry = taskMgmt.allTasks.find(...); if (entry)
taskMgmt.handleSelectTask(entry.task); }, 100)) is fragile; replace this timing
workaround by sequencing the selection after project activation completes (e.g.,
via a callback, returned Promise, or an effect that watches the activated
project) so selection happens deterministically. Modify the code path that
activates the project (the function that currently precedes this setTimeout) to
either accept a completion callback or return a Promise, then call
taskMgmt.handleSelectTask(entry.task) from that completion handler (or move the
selection into an effect that reacts to the active project change) instead of
relying on setTimeout; reference taskMgmt.allTasks and taskMgmt.handleSelectTask
and the similar pattern in handleTitlebarKanbanToggle when refactoring.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f96f059-01bd-4405-b1f3-b73a417eb777

📥 Commits

Reviewing files that changed from the base of the PR and between eead62b and d1ef2fe.

⛔ Files ignored due to path filters (3)
  • raycast-emdash/assets/icon.png is excluded by !**/*.png
  • raycast-emdash/icon.png is excluded by !**/*.png
  • raycast-emdash/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • raycast-emdash/.nvmrc
  • raycast-emdash/package.json
  • raycast-emdash/raycast-env.d.ts
  • raycast-emdash/src/lib/api.ts
  • raycast-emdash/src/lib/types.ts
  • raycast-emdash/src/recent-projects.tsx
  • raycast-emdash/src/search-projects.tsx
  • raycast-emdash/src/ssh-connections.tsx
  • raycast-emdash/src/tasks.tsx
  • raycast-emdash/tsconfig.json
  • src/main/app/lifecycle.ts
  • src/main/app/tray.ts
  • src/main/ipc/dbIpc.ts
  • src/main/main.ts
  • src/main/preload.ts
  • src/main/services/DataExportService.ts
  • src/main/services/DatabaseService.ts
  • src/main/services/LocalApiService.ts
  • src/renderer/types/electron-api.d.ts
  • src/renderer/views/Workspace.tsx

Comment thread raycast-emdash/.nvmrc
@@ -0,0 +1 @@
22.14.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Node.js version mismatch with project standards.

The specified version 22.14.0 differs from the project's standard of 22.20.0. Consider aligning with the main repository's Node.js version for consistency.

Based on learnings: "Use Node.js version 22.20.0 for development"

🔧 Proposed fix
-22.14.0
+22.20.0
📝 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.

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

In `@raycast-emdash/.nvmrc` at line 1, Update the Node.js version specified in the
.nvmrc from "22.14.0" to the project's standard "22.20.0" so the repository uses
the consistent runtime; locate the .nvmrc entry containing "22.14.0" and replace
it with "22.20.0" (also check any package.json engines field if present to keep
them in sync).

@@ -0,0 +1,38 @@
import { List, ActionPanel, Action } from '@raycast/api';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "ssh-connections.tsx" -type f

Repository: generalaction/emdash

Length of output: 104


🏁 Script executed:

cat -n ./raycast-emdash/src/ssh-connections.tsx

Repository: generalaction/emdash

Length of output: 1345


🌐 Web query:

Raycast API open function deep links documentation

💡 Result:

  • open() function (Utilities) — API reference for open(target, application?) (opens a file/folder/URL; optional app override). [1]
  • Deeplinks (lifecycle/info doc) — Deep link format raycast://extensions/<owner>/<extension>/<command> plus supported query params like launchType, arguments, context, and fallbackText. [2]
  • createDeeplink() helper — Utility to generate Raycast deeplinks programmatically (current extension / other extension / script command), including options for launchType, arguments, and fallbackText. [3]
  • Manual: Deeplinks overview — User-facing explanation and examples; points to developer docs for advanced usage. [4]

Sources:
[1] https://developers.raycast.com/api-reference/utilities
[2] https://developers.raycast.com/information/lifecycle/deeplinks
[3] https://developers.raycast.com/utilities/functions/createdeeplink
[4] https://manual.raycast.com/deeplinks


🏁 Script executed:

# Search for how open is used or exported in the codebase
rg "open" ./raycast-emdash --type tsx --type ts -A 2 -B 2 | head -50

Repository: generalaction/emdash

Length of output: 91


🏁 Script executed:

rg "open" ./raycast-emdash -A 2 -B 2 | head -80

Repository: generalaction/emdash

Length of output: 5260


Import open from @raycast/api before using it in the Connect action.

Line 31 calls open(...), but this import is missing from line 1. TypeScript will fail to compile with Cannot find name 'open'. Other files in this codebase (tasks.tsx, search-projects.tsx, recent-projects.tsx) correctly import this utility from @raycast/api.

🩹 Suggested change
-import { List, ActionPanel, Action } from '@raycast/api';
+import { List, ActionPanel, Action, open } from '@raycast/api';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raycast-emdash/src/ssh-connections.tsx` at line 1, The Connect action calls
open(...) but the symbol isn't imported; update the top import statement that
currently reads "import { List, ActionPanel, Action } from '@raycast/api'" to
also include open so TypeScript can resolve it; locate the Connect action (the
component/function that invokes open on line ~31) to confirm usage and then add
open to the import list from '@raycast/api'.

Comment on lines +15 to +16
"include": ["src/**/*"],
"exclude": ["node_modules"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "raycast-env.d.ts" -type f 2>/dev/null

Repository: generalaction/emdash

Length of output: 97


🏁 Script executed:

cat -n raycast-emdash/tsconfig.json

Repository: generalaction/emdash

Length of output: 562


🏁 Script executed:

git ls-files | grep -E "(tsconfig|raycast-env)" | head -20

Repository: generalaction/emdash

Length of output: 225


🏁 Script executed:

cat -n raycast-emdash/raycast-env.d.ts

Repository: generalaction/emdash

Length of output: 1597


🏁 Script executed:

rg "raycast-env|Raycast" raycast-emdash/src --type ts --type tsx -l

Repository: generalaction/emdash

Length of output: 91


🏁 Script executed:

# Check if any source files import from or reference types from raycast-env.d.ts
rg "from.*raycast-env|import.*raycast-env" raycast-emdash/src

Repository: generalaction/emdash

Length of output: 46


🏁 Script executed:

cat -n raycast-emdash/tsconfig.json

Repository: generalaction/emdash

Length of output: 562


🏁 Script executed:

# Check if there's a parent tsconfig that this might extend
rg "extends" raycast-emdash/tsconfig.json

Repository: generalaction/emdash

Length of output: 46


🏁 Script executed:

# Check if the .d.ts file path pattern could match with any other include patterns
cat raycast-emdash/tsconfig.json | jq '.include, .exclude' 2>/dev/null || cat raycast-emdash/tsconfig.json

Repository: generalaction/emdash

Length of output: 101


🏁 Script executed:

# Verify TypeScript's include behavior - check TypeScript documentation or examples
# Let's also check if there are any references in package.json that might affect this
cat -n raycast-emdash/package.json | head -50

Repository: generalaction/emdash

Length of output: 1585


Include raycast-env.d.ts in the TypeScript program.

raycast-env.d.ts is at the package root and contains ambient type declarations for Preferences and Arguments. The current include glob ["src/**/*"] does not match root-level files, so these types are unavailable to the TypeScript program.

🩹 Suggested change
-  "include": ["src/**/*"],
+  "include": ["src/**/*", "raycast-env.d.ts"],
📝 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.

Suggested change
"include": ["src/**/*"],
"exclude": ["node_modules"]
"include": ["src/**/*", "raycast-env.d.ts"],
"exclude": ["node_modules"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raycast-emdash/tsconfig.json` around lines 15 - 16, The tsconfig.json
currently only includes "src/**/*", so the ambient declarations in
raycast-env.d.ts at the package root aren't picked up; update the tsconfig
"include" to also cover the root declaration (e.g., add "raycast-env.d.ts" or a
root glob like "*.d.ts") so the ambient types from raycast-env.d.ts are included
in the TypeScript program and available to code referencing
Preferences/Arguments.

Comment thread src/main/app/lifecycle.ts
Comment on lines +6 to +10
app.whenReady().then(() => {
const win = createMainWindow();
setMainWindowRef(win);
createTray();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Keep main-window creation in a single startup path.

src/main/main.ts already calls createMainWindow() at Line 428, after database/service setup and IPC registration at Line 399. Creating another window here can both show the renderer before those handlers exist and leave you with two startup windows.

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

In `@src/main/app/lifecycle.ts` around lines 6 - 10, The app.whenReady() block in
lifecycle.ts is creating a second startup window by calling createMainWindow();
remove that duplicate call and only perform startup tasks that are safe to run
here (e.g., createTray()). Specifically, delete or stop invoking
createMainWindow() and setMainWindowRef(win) from this file so the single-window
creation remains in src/main/main.ts (where createMainWindow() and IPC/database
setup occur); ensure createTray() remains here (or is moved if desired) and that
any code relying on the main window reference uses the setMainWindowRef call
only from the main.ts startup path where the window is actually created.

Comment thread src/main/app/tray.ts
Comment on lines +40 to +48
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
} else {
const { createMainWindow } = require('./window');
createMainWindow();
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Window reference not updated after dynamic creation.

When createMainWindow() is called via the context menu, the newly created window is not stored in the local mainWindow variable. This means subsequent double-click events (lines 62-67) won't show the window since mainWindow will still be null.

🐛 Proposed fix to capture the new window reference
       click: () => {
         if (mainWindow) {
           mainWindow.show();
           mainWindow.focus();
         } else {
           const { createMainWindow } = require('./window');
-          createMainWindow();
+          const win = createMainWindow();
+          setMainWindowRef(win);
         }
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/app/tray.ts` around lines 40 - 48, The tray menu click handler
doesn't update the local mainWindow reference when creating the window
dynamically; modify the click branch so it captures and stores the created
window: require('./window') to get createMainWindow and assign its return to the
local mainWindow variable (i.e., mainWindow = createMainWindow()); ensure
createMainWindow returns the new BrowserWindow instance (update createMainWindow
implementation if necessary) so subsequent handlers (e.g., the tray double-click
logic that checks mainWindow) will operate on the new window.

Comment thread src/main/main.ts
Comment on lines +327 to +329
// Register deep link handler (emdash://)
app.setAsDefaultProtocolClient('emdash');
app.on('open-url', async (event, url) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the code around lines 327-329 in src/main/main.ts
head -340 src/main/main.ts | tail -30

Repository: generalaction/emdash

Length of output: 921


🏁 Script executed:

# Get more context around the whenReady() block
sed -n '300,350p' src/main/main.ts

Repository: generalaction/emdash

Length of output: 1625


🏁 Script executed:

# Look for app.whenReady() to understand the initialization flow
rg "app\.whenReady|setAsDefaultProtocolClient|open-url" src/main/main.ts -B 2 -A 5

Repository: generalaction/emdash

Length of output: 930


🏁 Script executed:

# Check Electron version and any related comments
grep -n "electron" src/main/main.ts | head -20

Repository: generalaction/emdash

Length of output: 121


🏁 Script executed:

# Check Electron version in package.json
rg '"electron"' -A 1 -B 1

Repository: generalaction/emdash

Length of output: 372


🌐 Web query:

Electron app.on('open-url') listener timing macOS app.whenReady() protocol handler

💡 Result:

On macOS, the open-url event can be emitted before ready / app.whenReady() resolves (e.g., when the app is cold-started by a protocol link). If you register app.on('open-url', ...) only inside/after whenReady(), you can miss that first URL. Electron’s app docs explicitly say to register the open-url listener “early” and that registering in response to ready can miss launch URLs. [1]

Correct pattern (macOS): register immediately (or in will-finish-launching), then defer handling until your UI is ready:

const { app } = require('electron')

let pendingDeepLink = null

// Register as early as possible (top-level) to avoid missing cold-start URLs.
app.on('open-url', (event, url) => {
  event.preventDefault()
  pendingDeepLink = url
  // If your window exists already, handle it immediately here too.
})

// Optional: many people attach the handler during will-finish-launching on macOS
// to make the “early” requirement very explicit.
app.on('will-finish-launching', () => {
  // could attach open-url here instead of top-level
})

app.whenReady().then(() => {
  createWindow()
  if (pendingDeepLink) {
    handleDeepLink(pendingDeepLink)
    pendingDeepLink = null
  }
})

Notes:

  • will-finish-launching happens during early startup (on macOS it maps to applicationWillFinishLaunching). [1]
  • The official deep-link tutorial shows app.on('open-url') alongside app.whenReady() for macOS, but the app API docs are the key detail about timing: don’t wait for ready to attach the listener if you need cold-start URLs. [1][2]
  • A common workaround cited in practice is attaching open-url from within will-finish-launching specifically to ensure it’s in place before ready. [3]

Sources: Electron app event docs (open-url, will-finish-launching, ready) [1], Electron deep links tutorial [2], discussion/example emphasizing will-finish-launching timing [3].

[1] https://www.electronjs.org/docs/latest/api/app
[2] https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app
[3] https://stackoverflow.com/questions/53706348/electron-open-url-event-isnt-triggered-while-app-isnt-running-on-background


Move the open-url listener outside the whenReady() callback to avoid missing cold-start protocol events on macOS.

On macOS, a protocol launch from Raycast can arrive before app.whenReady() completes. Registering the listener only inside the whenReady() callback means the first emdash://... event will be dropped before the listener is attached. Register app.on('open-url', ...) at the top level or in app.on('will-finish-launching') to ensure it's in place early, then defer window creation and URL handling until the whenReady() callback executes (using a pattern like storing the pending URL and processing it once the UI is ready).

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

In `@src/main/main.ts` around lines 327 - 329, The open-url listener is currently
registered inside app.whenReady(), which can miss cold-start protocol events on
macOS; move the app.on('open-url', ...) registration out of whenReady() (or
attach it in app.on('will-finish-launching')) so it runs early, capture the
incoming url into a module-scoped pendingUrl variable, and then in
app.whenReady() call your existing URL processing logic (e.g., invoke
handleOpenUrl(pendingUrl) or the same handler used by the listener) after the
window/UI is created; keep app.setAsDefaultProtocolClient('emdash') where it is
but ensure the listener and pendingUrl/handleOpenUrl symbols are used to defer
handling until ready.

Comment thread src/main/main.ts
Comment on lines +329 to +331
app.on('open-url', async (event, url) => {
event.preventDefault();
console.log('Deep link received:', url);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redact deep-link payloads before logging.

For project= links, the URL contains the user’s local filesystem path. Logging the raw string here leaks usernames/repo paths into app logs on every Raycast launch.

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

In `@src/main/main.ts` around lines 329 - 331, The open-url handler
(app.on('open-url', async (event, url) => { ... })) currently logs the raw url
which can leak local filesystem paths for project= links; parse the incoming url
(the url variable) and, if it contains a project= query value, redact or replace
the path portion (e.g., strip directories and/or replace with "<REDACTED_PATH>"
or only keep the repo name) before calling console.log; update the log call to
use the sanitizedUrl so other URLs still log normally while project= payloads
never expose full local paths.

Comment thread src/main/main.ts
Comment on lines +335 to +391
const parsedUrl = new URL(url);
const projectPath = parsedUrl.searchParams.get('project');
const taskId = parsedUrl.searchParams.get('task');

let win = BrowserWindow.getAllWindows()[0];

// If no window exists, create one
if (!win) {
const { createMainWindow } = await import('./app/window');
win = createMainWindow();
// Wait for window to be ready
await new Promise((resolve) => {
win?.once('ready-to-show', resolve);
// Fallback timeout
setTimeout(resolve, 1000);
});
}

if (win) {
// Show and focus window
win.show();
win.focus();

if (projectPath) {
// Open project by path
const project = await databaseService.getProjectByPath(projectPath);
if (project) {
win.webContents.send('deep-link', { type: 'open-project', project });
} else {
// Project not in DB, save it first
const projectName = projectPath.split('/').pop() || 'Unknown';
await databaseService.saveProject({
id: `dl-${Date.now()}`,
name: projectName,
path: projectPath,
gitInfo: { isGitRepo: false },
});
const newProject = await databaseService.getProjectByPath(projectPath);
if (newProject) {
win.webContents.send('deep-link', { type: 'open-project', project: newProject });
}
}
} else if (taskId) {
// Fetch task and its project
const task = await databaseService.getTaskById(taskId);
if (task) {
const project = await databaseService.getProjectById(task.projectId);
win.webContents.send('deep-link', {
type: 'open-task',
taskId,
projectId: task.projectId,
project: project || undefined,
});
} else {
win.webContents.send('deep-link', { type: 'open-task', taskId });
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Handle SSH deep links in this parser as well.

raycast-emdash/src/ssh-connections.tsx Line 31 opens emdash://ssh?connection=${conn.id}, but this handler only reads project and task. The new SSH command currently opens the app and then stops.

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

In `@src/main/main.ts` around lines 335 - 391, The deep-link parser (parsedUrl /
projectPath / taskId) currently ignores SSH links; add handling for the SSH
query parameter (e.g., connectionId = parsedUrl.searchParams.get('connection'))
alongside project/task branches. After ensuring win is ready, detect
connectionId and call the appropriate DB accessor (e.g.,
databaseService.getSshConnectionById or similar), then send a deep-link via
win.webContents.send('deep-link', { type: 'open-ssh', connection: connection ||
undefined, connectionId }) so the renderer can open the SSH connection; if the
DB accessor returns null just send the connectionId so the UI can handle missing
entries. Ensure you reference parsedUrl, databaseService, getSshConnectionById
(or the existing SSH lookup method), and the 'deep-link' IPC channel.

Comment on lines +81 to +91
tasks: tasks.map((t) => ({
id: t.id,
projectId: t.projectId,
name: t.name,
branch: t.branch,
path: t.path,
status: t.status,
agentId: t.agentId,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
})),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing archivedAt field in exported tasks.

The Task interface in raycast-emdash/src/lib/types.ts includes archivedAt?: string | null, and the Raycast API's getTasks() method filters tasks using !t.archivedAt. However, the export mapping here omits archivedAt, which means the Raycast extension will receive tasks without this field (always undefined), causing all tasks to pass the filter.

🐛 Proposed fix to include archivedAt
       tasks: tasks.map((t) => ({
         id: t.id,
         projectId: t.projectId,
         name: t.name,
         branch: t.branch,
         path: t.path,
         status: t.status,
         agentId: t.agentId,
+        archivedAt: t.archivedAt,
         createdAt: t.createdAt,
         updatedAt: t.updatedAt,
       })),
📝 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.

Suggested change
tasks: tasks.map((t) => ({
id: t.id,
projectId: t.projectId,
name: t.name,
branch: t.branch,
path: t.path,
status: t.status,
agentId: t.agentId,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
})),
tasks: tasks.map((t) => ({
id: t.id,
projectId: t.projectId,
name: t.name,
branch: t.branch,
path: t.path,
status: t.status,
agentId: t.agentId,
archivedAt: t.archivedAt,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
})),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/DataExportService.ts` around lines 81 - 91, The exported
task objects in DataExportService (the tasks.map transform) are missing the
archivedAt property, so Raycast receives tasks with archivedAt undefined; update
the mapping inside DataExportService (the tasks: tasks.map(...) block) to
include archivedAt: t.archivedAt so the exported tasks preserve their archived
status (nullable) and Raycast's !t.archivedAt filter behaves correctly.

@@ -0,0 +1,179 @@
import http from 'http';
import { URL } from 'url';
import { BrowserWindow } from 'electron';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused import.

BrowserWindow is imported but never used in this file.

🧹 Proposed fix
-import { BrowserWindow } from 'electron';
📝 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.

Suggested change
import { BrowserWindow } from 'electron';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/LocalApiService.ts` at line 3, Remove the unused
BrowserWindow import from LocalApiService.ts: locate the import line that reads
"import { BrowserWindow } from 'electron';" and delete it (or remove
BrowserWindow from the import list) so the file no longer imports an unused
symbol; ensure no other code in LocalApiService references BrowserWindow after
this change.

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