Skip to content

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

Add morgen extension #17449

wants to merge 2 commits into from

Conversation

Zhehan-Z
Copy link

@Zhehan-Z Zhehan-Z commented Feb 28, 2025

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

  • Task/Event Creation: Not supported due to current API constraints.
  • Search Range: Limited to 7 days to manage API rate limits.

Roadmap

  • API Expansion & Feature Requests
    • Enable task and event creation/updating via API.
    • Introduce AI scheduling functionalities within Raycast.
  • Enhanced Search Capabilities
    • Extend search range and improve filtering.
    • Cache events locally for better performance.
  • Task & Event Management
    • Implement direct creation, editing, and RSVP actions (pending API updates).
  • Scheduler Integration
    • Deepen integration with Morgen’s scheduler for seamless booking links.

Screencast

morgen-1
Enter an API key on welcome page

morgen-2
Search for events & tasks

morgen-4
All currently available commands

CleanShot 2025-03-01 at 04 45 41
Easy deep linking to key pages in Morgen App

Raycast 2025-03-01 at 04 14 39
Use with Ask AI tools

Checklist

- Fix more style issues
- Fix style issues
- Fix bad AI tool
- Fix style issues
- Initialize code
- Initial commit
@raycastbot raycastbot added the new extension Label for PRs with new extensions label Feb 28, 2025
@raycastbot
Copy link
Collaborator

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.

@ransurf
Copy link

ransurf commented Mar 5, 2025

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

@Zhehan-Z
Copy link
Author

Zhehan-Z commented Mar 5, 2025 via email

@pernielsentikaer
Copy link
Collaborator

@greptileai can you check this?

Copy link
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.

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 in package.json needs AI evals - please add at least one eval following the documentation
  • In src/api/morgen.ts, request function should be wrapped in try/catch with showFailureToast from @raycast/utils for better error handling
  • In src/search-morgen-events-and-tasks.tsx, the List should use isLoading prop to avoid empty state flicker per guidelines
  • Since there are multiple commands, consider adding subtitle in package.json commands using the service title "Morgen" for better context
  • The metadata folder with screenshots is required since there are view 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

Comment on lines +163 to +166
await this.request(`/events/delete?seriesUpdateMode=single`, {
method: "POST",
body: JSON.stringify(payload),
});
Copy link
Contributor

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).
Copy link
Contributor

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

Suggested change
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)
Copy link
Contributor

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."
Copy link
Contributor

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

Suggested change
"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."

Comment on lines +38 to +44
"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."
}
],
Copy link
Contributor

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

Comment on lines +91 to +96
// 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]));
Copy link
Contributor

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

Comment on lines +307 to +320
// 以下代码用于计算时区偏移但未被使用,可以删除
// 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]);

Copy link
Contributor

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

Suggested change
// 以下代码用于计算时区偏移但未被使用,可以删除
// 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);

Comment on lines +259 to +325
// 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;
}
Copy link
Contributor

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

Comment on lines +165 to +166
<List isLoading={state.isLoading} selectedItemId={state.selectedId}>
{/* Upcoming Section */}
Copy link
Contributor

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

Comment on lines +152 to +158
if (state.error) {
return (
<List>
<List.EmptyView icon={Icon.ExclamationMark} title="Failed to load events" description={state.error.message} />
</List>
);
}
Copy link
Contributor

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

@Zhehan-Z
Copy link
Author

Zhehan-Z commented Mar 5, 2025

@greptileai can you check this?

Thank you for triggering an AI check. I'll head out to solve the enlisted issues.

Copy link
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.

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 as acheive in the tools description in package.json
  • The getCalendars() and getEvents() API calls in search-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

Comment on lines +1 to +4
{
"printWidth": 120,
"singleQuote": false
}
Copy link
Contributor

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

Comment on lines +13 to +15
await open("morgen://open-sidebar-scheduler");
await showHUD("Opening Booking Links in Morgen");
return;
Copy link
Contributor

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");
Copy link
Contributor

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

Comment on lines +8 to +47
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);
}
Copy link
Contributor

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

Comment on lines +327 to +333
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;
}
Copy link
Contributor

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

Suggested change
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)?/);
Copy link
Contributor

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

@nro337
Copy link

nro337 commented Mar 6, 2025

What a great implementation!
After this launches, it would be great to have an Edit Event function as well. Could always make a PR myself as well

@andreaselia
Copy link
Contributor

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?

@nro337
Copy link

nro337 commented Apr 26, 2025

Thanks for checking, also curious about this!

@Zhehan-Z
Copy link
Author

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?

Yes. It's pending on Morgen's side. They have other priorities before rolling out an essential API.

@andreaselia andreaselia marked this pull request as draft April 27, 2025 07:05
@andreaselia
Copy link
Contributor

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! 🙌

@Zhehan-Z
Copy link
Author

Zhehan-Z commented Apr 27, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
AI Extension new extension Label for PRs with new extensions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants