feat: initialize Emdash extension with core files and API :#1564
feat: initialize Emdash extension with core files and API :#1564yashdev9274 wants to merge 1 commit intogeneralaction:mainfrom
Conversation
- 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.
|
@yashdev9274 is attempting to deploy a commit to the General Action Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
📝 WalkthroughWalkthroughThis pull request introduces a Raycast extension for browsing Emdash projects, tasks, and SSH connections, alongside support for Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (8)
src/main/services/LocalApiService.ts (2)
123-127: UsegetProjectByIdfor 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: UsegetProjectByIdfor single project lookup.Fetching all projects and filtering client-side is inefficient. The
DatabaseServicealready has agetProjectByIdmethod 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: ThesetTimeoutdelay 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 ifprocess.resourcesPathis undefined.If
process.resourcesPathis undefined in certain Electron configurations, the fallback to empty string ('') will produce an invalid pathicon.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, andreadFileSyncare blocking operations used inside anasyncmethod. 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
⛔ Files ignored due to path filters (3)
raycast-emdash/assets/icon.pngis excluded by!**/*.pngraycast-emdash/icon.pngis excluded by!**/*.pngraycast-emdash/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (20)
raycast-emdash/.nvmrcraycast-emdash/package.jsonraycast-emdash/raycast-env.d.tsraycast-emdash/src/lib/api.tsraycast-emdash/src/lib/types.tsraycast-emdash/src/recent-projects.tsxraycast-emdash/src/search-projects.tsxraycast-emdash/src/ssh-connections.tsxraycast-emdash/src/tasks.tsxraycast-emdash/tsconfig.jsonsrc/main/app/lifecycle.tssrc/main/app/tray.tssrc/main/ipc/dbIpc.tssrc/main/main.tssrc/main/preload.tssrc/main/services/DataExportService.tssrc/main/services/DatabaseService.tssrc/main/services/LocalApiService.tssrc/renderer/types/electron-api.d.tssrc/renderer/views/Workspace.tsx
| @@ -0,0 +1 @@ | |||
| 22.14.0 | |||
There was a problem hiding this comment.
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.
| 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'; | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "ssh-connections.tsx" -type fRepository: generalaction/emdash
Length of output: 104
🏁 Script executed:
cat -n ./raycast-emdash/src/ssh-connections.tsxRepository: generalaction/emdash
Length of output: 1345
🌐 Web query:
Raycast API open function deep links documentation
💡 Result:
open()function (Utilities) — API reference foropen(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 likelaunchType,arguments,context, andfallbackText. [2] createDeeplink()helper — Utility to generate Raycast deeplinks programmatically (current extension / other extension / script command), including options forlaunchType,arguments, andfallbackText. [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 -50Repository: generalaction/emdash
Length of output: 91
🏁 Script executed:
rg "open" ./raycast-emdash -A 2 -B 2 | head -80Repository: 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'.
| "include": ["src/**/*"], | ||
| "exclude": ["node_modules"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "raycast-env.d.ts" -type f 2>/dev/nullRepository: generalaction/emdash
Length of output: 97
🏁 Script executed:
cat -n raycast-emdash/tsconfig.jsonRepository: generalaction/emdash
Length of output: 562
🏁 Script executed:
git ls-files | grep -E "(tsconfig|raycast-env)" | head -20Repository: generalaction/emdash
Length of output: 225
🏁 Script executed:
cat -n raycast-emdash/raycast-env.d.tsRepository: generalaction/emdash
Length of output: 1597
🏁 Script executed:
rg "raycast-env|Raycast" raycast-emdash/src --type ts --type tsx -lRepository: 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/srcRepository: generalaction/emdash
Length of output: 46
🏁 Script executed:
cat -n raycast-emdash/tsconfig.jsonRepository: generalaction/emdash
Length of output: 562
🏁 Script executed:
# Check if there's a parent tsconfig that this might extend
rg "extends" raycast-emdash/tsconfig.jsonRepository: 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.jsonRepository: 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 -50Repository: 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.
| "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.
| app.whenReady().then(() => { | ||
| const win = createMainWindow(); | ||
| setMainWindowRef(win); | ||
| createTray(); | ||
| }); |
There was a problem hiding this comment.
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.
| click: () => { | ||
| if (mainWindow) { | ||
| mainWindow.show(); | ||
| mainWindow.focus(); | ||
| } else { | ||
| const { createMainWindow } = require('./window'); | ||
| createMainWindow(); | ||
| } | ||
| }, |
There was a problem hiding this comment.
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.
| // Register deep link handler (emdash://) | ||
| app.setAsDefaultProtocolClient('emdash'); | ||
| app.on('open-url', async (event, url) => { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, examine the code around lines 327-329 in src/main/main.ts
head -340 src/main/main.ts | tail -30Repository: generalaction/emdash
Length of output: 921
🏁 Script executed:
# Get more context around the whenReady() block
sed -n '300,350p' src/main/main.tsRepository: 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 5Repository: generalaction/emdash
Length of output: 930
🏁 Script executed:
# Check Electron version and any related comments
grep -n "electron" src/main/main.ts | head -20Repository: generalaction/emdash
Length of output: 121
🏁 Script executed:
# Check Electron version in package.json
rg '"electron"' -A 1 -B 1Repository: 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-launchinghappens during early startup (on macOS it maps toapplicationWillFinishLaunching). [1]- The official deep-link tutorial shows
app.on('open-url')alongsideapp.whenReady()for macOS, but theappAPI docs are the key detail about timing: don’t wait forreadyto attach the listener if you need cold-start URLs. [1][2] - A common workaround cited in practice is attaching
open-urlfrom withinwill-finish-launchingspecifically to ensure it’s in place beforeready. [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.
| app.on('open-url', async (event, url) => { | ||
| event.preventDefault(); | ||
| console.log('Deep link received:', url); |
There was a problem hiding this comment.
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.
| 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 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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, | ||
| })), |
There was a problem hiding this comment.
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.
| 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'; | |||
There was a problem hiding this comment.
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.
| 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.
Summary
Fixes
new feat- Raycast Emdash extension with commands: search /open projects, recent projects, task, ssh connection
Snapshot
emdash-raycast.mp4
Type of change
Mandatory Tasks
Checklist
pnpm run format)pnpm run lint)Summary by CodeRabbit
Release Notes
emdash://protocol to open projects and tasks from external applications