-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add morgen extension #17449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add morgen extension #17449
Conversation
- Fix more style issues - Fix style issues - Fix bad AI tool - Fix style issues - Initialize code - Initial commit
Congratulations on your new Raycast extension! 🚀 Due to our current reduced availability, the initial review may take up to 10-15 business days Once the PR is approved and merged, the extension will be available on our Store. |
hey @Zhehan-Z awesome work and thank you for creating this! @AlaaMouch shared it with the team in Slack, the devs at Morgen are excited to try out once it's available :) |
Thank you! I’m actively working to improve its performance now. I hope it will be usable as a day-to-day tool soon.
|
@greptileai can you check this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
This PR adds a new Morgen calendar extension for Raycast that enables users to view their schedule, search events/tasks, and quickly access key Morgen views through deep linking.
- The
tools
section inpackage.json
needs AIevals
- please add at least one eval following the documentation - In
src/api/morgen.ts
,request
function should be wrapped in try/catch withshowFailureToast
from@raycast/utils
for better error handling - In
src/search-morgen-events-and-tasks.tsx
, the List should useisLoading
prop to avoid empty state flicker per guidelines - Since there are multiple commands, consider adding
subtitle
inpackage.json
commands using the service title "Morgen" for better context - The
metadata
folder with screenshots is required since there areview
commands - please add following documentation
💡 (1/5) You can manually trigger the bot by mentioning @greptileai in a comment!
15 file(s) reviewed, 18 comment(s)
Edit PR Review Bot Settings | Greptile
await this.request(`/events/delete?seriesUpdateMode=single`, { | ||
method: "POST", | ||
body: JSON.stringify(payload), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: deleteEvent response should be properly typed and checked even though it returns void
|
||
**To start, enter your API key obtained from [https://platform.morgen.so/developers-api](https://platform.morgen.so/developers-api).** | ||
|
||
Haven't subscribed to Morgen Calendar? Start with a free trial using [this link](https://www.morgen.so/?ref=yzq5y2z). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: The referral link 'yzq5y2z' appears to be a personal referral code. Consider using a generic link instead
Haven't subscribed to Morgen Calendar? Start with a free trial using [this link](https://www.morgen.so/?ref=yzq5y2z). | |
Haven't subscribed to Morgen Calendar? Start with a free trial using [this link](https://www.morgen.so). |
- Add quick task shortcuts for instant scheduling. | ||
- Event Management Enhancements | ||
- Enable direct event creation and modifications (pending _better_ API support). | ||
- Allow quick RSVP actions for event invitations. (WIP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: WIP items should be moved to a separate section or removed from the public documentation
{ | ||
"name": "search-morgen-events-and-tasks", | ||
"title": "Search Morgen Events and Tasks", | ||
"description": "Use all contents from Morgen calendar, including all events and tasks, to help user acheive their goals." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syntax: 'acheive' is misspelled in the tools description
"description": "Use all contents from Morgen calendar, including all events and tasks, to help user acheive their goals." | |
"description": "Use all contents from Morgen calendar, including all events and tasks, to help user achieve their goals." |
"tools": [ | ||
{ | ||
"name": "search-morgen-events-and-tasks", | ||
"title": "Search Morgen Events and Tasks", | ||
"description": "Use all contents from Morgen calendar, including all events and tasks, to help user acheive their goals." | ||
} | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: tools section needs an 'ai' object with 'evals' inside. See https://developers.raycast.com/ai/write-evals-for-your-ai-extension
// Get events | ||
const events = await api.getEvents(startDateISO, endDateISO); | ||
|
||
// Get calendar information for display | ||
const calendars = await api.getCalendars(); | ||
const calendarMap = new Map(calendars.map((cal) => [cal.id, cal])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Multiple sequential API calls could be parallelized with Promise.all for better performance
// 以下代码用于计算时区偏移但未被使用,可以删除 | ||
// Calculate timezone offset (this method isn't the most accurate, but works for most cases) | ||
// Get ISO strings for both timezones | ||
sourceFormatter.format(eventDate); | ||
localFormatter.format(eventDate); | ||
|
||
// 删除未使用的变量解析代码 | ||
// const sourceTime = sourceFormatter.format(eventDate); | ||
// const localTime = localFormatter.format(eventDate); | ||
// const sourceHour = parseInt(sourceTime.split(", ")[1].split(":")[0]); | ||
// const sourceMinute = parseInt(sourceTime.split(", ")[1].split(":")[1]); | ||
// const localHour = parseInt(localTime.split(", ")[1].split(":")[0]); | ||
// const localMinute = parseInt(localTime.split(", ")[1].split(":")[1]); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Remove commented out code and Chinese comments - they add noise and are not needed
// 以下代码用于计算时区偏移但未被使用,可以删除 | |
// Calculate timezone offset (this method isn't the most accurate, but works for most cases) | |
// Get ISO strings for both timezones | |
sourceFormatter.format(eventDate); | |
localFormatter.format(eventDate); | |
// 删除未使用的变量解析代码 | |
// const sourceTime = sourceFormatter.format(eventDate); | |
// const localTime = localFormatter.format(eventDate); | |
// const sourceHour = parseInt(sourceTime.split(", ")[1].split(":")[0]); | |
// const sourceMinute = parseInt(sourceTime.split(", ")[1].split(":")[1]); | |
// const localHour = parseInt(localTime.split(", ")[1].split(":")[0]); | |
// const localMinute = parseInt(localTime.split(", ")[1].split(":")[1]); | |
sourceFormatter.format(eventDate); | |
localFormatter.format(eventDate); |
// Completely rewritten timezone conversion function using more reliable methods | ||
function convertTimeZoneToLocal(dateString: string, timeZone: string): Date { | ||
// Parse date and time parts | ||
const [datePart, timePart] = dateString.split("T"); | ||
const [year, month, day] = datePart.split("-").map(Number); | ||
const [hours, minutes, seconds] = timePart.split(":").map(Number); | ||
|
||
// Get current system timezone | ||
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
|
||
// Create object representing specified date and time in source timezone | ||
const eventDate = new Date( | ||
Date.UTC( | ||
year, | ||
month - 1, // Months are 0-11 | ||
day, | ||
hours, | ||
minutes, | ||
seconds || 0, | ||
), | ||
); | ||
|
||
// Format using source timezone | ||
const sourceFormatter = new Intl.DateTimeFormat("en-US", { | ||
timeZone, | ||
year: "numeric", | ||
month: "numeric", | ||
day: "numeric", | ||
hour: "numeric", | ||
minute: "numeric", | ||
second: "numeric", | ||
hour12: false, | ||
timeZoneName: "short", | ||
}); | ||
|
||
// Format using local timezone | ||
const localFormatter = new Intl.DateTimeFormat("en-US", { | ||
timeZone: localTimeZone, | ||
year: "numeric", | ||
month: "numeric", | ||
day: "numeric", | ||
hour: "numeric", | ||
minute: "numeric", | ||
second: "numeric", | ||
hour12: false, | ||
timeZoneName: "short", | ||
}); | ||
|
||
// 以下代码用于计算时区偏移但未被使用,可以删除 | ||
// Calculate timezone offset (this method isn't the most accurate, but works for most cases) | ||
// Get ISO strings for both timezones | ||
sourceFormatter.format(eventDate); | ||
localFormatter.format(eventDate); | ||
|
||
// 删除未使用的变量解析代码 | ||
// const sourceTime = sourceFormatter.format(eventDate); | ||
// const localTime = localFormatter.format(eventDate); | ||
// const sourceHour = parseInt(sourceTime.split(", ")[1].split(":")[0]); | ||
// const sourceMinute = parseInt(sourceTime.split(", ")[1].split(":")[1]); | ||
// const localHour = parseInt(localTime.split(", ")[1].split(":")[0]); | ||
// const localMinute = parseInt(localTime.split(", ")[1].split(":")[1]); | ||
|
||
// Create correct local date object | ||
const correctLocalDate = new Date(year, month - 1, day, hours, minutes, seconds || 0); | ||
|
||
return correctLocalDate; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: The timezone conversion function creates formatters but doesn't use them for actual conversion, which could lead to incorrect times
<List isLoading={state.isLoading} selectedItemId={state.selectedId}> | ||
{/* Upcoming Section */} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Lists should use isShowingDetail to avoid empty state flicker: https://developers.raycast.com/basics/prepare-an-extension-for-store#empty-states
if (state.error) { | ||
return ( | ||
<List> | ||
<List.EmptyView icon={Icon.ExclamationMark} title="Failed to load events" description={state.error.message} /> | ||
</List> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Could be simplified with showFailureToast from @raycast/utils
Thank you for triggering an AI check. I'll head out to solve the enlisted issues. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
Based on the latest changes and avoiding repetition from the previous review, here's my additional feedback:
The Morgen extension adds calendar integration with search and deep linking capabilities, with recent updates to the implementation.
- The timezone conversion in
src/search-morgen-events-and-tasks.tsx
needs improvement as the current implementation may cause issues with daylight savings and edge cases - The
launchCommand
calls in the open-morgen-*.tsx files should be wrapped in try/catch blocks per Raycast guidelines - The
achieve
is misspelled asacheive
in the tools description inpackage.json
- The
getCalendars()
andgetEvents()
API calls insearch-morgen-events-and-tasks.ts
could be parallelized for better performance
Note: I've focused only on new issues not mentioned in the previous review to avoid redundancy.
15 file(s) reviewed, 6 comment(s)
Edit PR Review Bot Settings | Greptile
{ | ||
"printWidth": 120, | ||
"singleQuote": false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider adding trailingComma: "es5"
to ensure consistent comma usage across the codebase
await open("morgen://open-sidebar-scheduler"); | ||
await showHUD("Opening Booking Links in Morgen"); | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider using showFailureToast from @raycast/utils for error handling instead of showHUD
await showHUD("Opening Invitations in Morgen"); | ||
return; | ||
} catch (error) { | ||
console.log("URL scheme failed, trying alternative methods"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Error from URL scheme failure should be logged with console.error for consistency with other error handling in the file
try { | ||
// Try multiple methods to open Morgen | ||
|
||
// Method 1: Try direct URL scheme | ||
try { | ||
await open("morgen://open-sidebar-flex"); | ||
await showHUD("Opening Invitations in Morgen"); | ||
return; | ||
} catch (error) { | ||
console.log("URL scheme failed, trying alternative methods"); | ||
} | ||
|
||
// Method 2: Open app first, then try AppleScript | ||
try { | ||
// First open Morgen app | ||
await open("Morgen"); | ||
|
||
// Use AppleScript to send URL scheme command to Morgen | ||
const script = ` | ||
tell application "Morgen" | ||
activate | ||
delay 1 | ||
open location "morgen://open-sidebar-flex" | ||
end tell | ||
`; | ||
|
||
await execPromise(`osascript -e '${script}'`); | ||
await showHUD("Opening Invitations in Morgen"); | ||
return; | ||
} catch (error) { | ||
console.error("AppleScript method failed:", error); | ||
} | ||
|
||
// Method 3: Just open the app as fallback | ||
await open("Morgen"); | ||
await showHUD("Opened Morgen (Invitations view not available)"); | ||
} catch (error) { | ||
await showHUD("Failed to open Morgen"); | ||
console.error("Error opening Morgen:", error); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Could be simplified using @raycast/utils showFailureToast in the catch blocks
function parseDuration(duration: string): number { | ||
const matches = duration.match(/PT(\d+)([HM])/i); | ||
if (!matches) return 0; | ||
const value = parseInt(matches[1]); | ||
const unit = matches[2].toUpperCase(); | ||
return unit === "H" ? value * 60 * 60 * 1000 : value * 60 * 1000; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Duration parsing doesn't handle combined hours and minutes (e.g. PT1H30M) which could lead to incorrect event durations
function parseDuration(duration: string): number { | |
const matches = duration.match(/PT(\d+)([HM])/i); | |
if (!matches) return 0; | |
const value = parseInt(matches[1]); | |
const unit = matches[2].toUpperCase(); | |
return unit === "H" ? value * 60 * 60 * 1000 : value * 60 * 1000; | |
} | |
function parseDuration(duration: string): number { | |
let totalMs = 0; | |
const hours = duration.match(/PT?(\d+)H/i); | |
const minutes = duration.match(/T?(?:\d+H)?(\d+)M/i); | |
if (hours) totalMs += parseInt(hours[1]) * 60 * 60 * 1000; | |
if (minutes) totalMs += parseInt(minutes[1]) * 60 * 1000; | |
return totalMs; | |
} |
const formattedEvents: EventResult[] = filteredEvents.map((event) => { | ||
// Calculate end time | ||
const startTime = parseISO(event.start); | ||
const durationMatch = event.duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?/); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider extracting ISO duration regex pattern into a named constant for better maintainability
What a great implementation! |
Hey @Zhehan-Z 👋 I take it this is currently pending a solution to your issue raised in the morgen-dev-docs repository, and in turn the Greptile feedback, before it's ready for another review? |
Thanks for checking, also curious about this! |
Yes. It's pending on Morgen's side. They have other priorities before rolling out an essential API. |
Thanks @Zhehan-Z, I've marked this PR as a draft for the time being. When you're ready for another review, please mark the PR as ready for review, and give me a ping so I can take another look 😁 Have a great rest of your weekend and week ahead! 🙌 |
Thanks! I’ll keep you posted when this gets out of draft.
张哲涵
Eric - Zhehan ZHANG
***@***.*** ***@***.***>
https://me.zheha.nz <https://me.zheha.nz/>
2025年4月27日 +0800 15:07 Andreas Elia ***@***.***>,写道:
[https://avatars.githubusercontent.com/u/5033092?s=20&v=4]andreaselia left a comment (raycast/extensions#17449)<#17449 (comment)>
Thanks @Zhehan-Z<https://github.com/Zhehan-Z>, I've marked this PR as a draft for the time being.
When you're ready for another review, please mark the PR as ready for review, and give me a ping so I can take another look 😁
Have a great rest of your weekend and week ahead! 🙌
—
Reply to this email directly, view it on GitHub<#17449 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AQBHPHZLJKW5JMCUTHZBCYD23R633AVCNFSM6AAAAABYDDZMTOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDQMZTGI2DOOJWGI>.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
Description
Morgen Raycast Extension
Overview
Morgen Calendar is an AI-powered daily planner that prioritizes one's most important to-dos, events, and projects in one app.
The Morgen Raycast extension integrates your Morgen schedule directly into Raycast, providing quick access to events, tasks, and key Morgen views. With seamless search functionality, users can efficiently navigate their schedules without leaving Raycast.
Getting Started
Enter a Morgen API key from Morgen's Developer API to enable the extension.
Known Limitations
Roadmap
Screencast
Enter an API key on welcome page
Search for events & tasks
All currently available commands
Easy deep linking to key pages in Morgen App
Use with Ask AI tools
Checklist
npm run build
and tested this distribution build in Raycastassets
folder are used by the extension itselfREADME
are placed outside of themetadata
folder