Conversation
- * ✨ feat: add new layouts, smart commands, save/restore, and custom layouts - Pull contributions
|
Thank you for your contribution! 🎉 🔔 @teemusuvinen you might want to have a look. You can use this guide to learn how to check out the Pull Request locally in order to test it. 📋 Quick checkout commandsBRANCH="ext/window-layouts"
FORK_URL="https://github.com/gfazioli/raycast-extensions.git"
EXTENSION_NAME="window-layouts"
REPO_NAME="raycast-extensions"
git clone -n --depth=1 --filter=tree:0 -b $BRANCH $FORK_URL
cd $REPO_NAME
git sparse-checkout set --no-cone "extensions/$EXTENSION_NAME"
git checkout
cd "extensions/$EXTENSION_NAME"
npm install && npm run devWe're currently experiencing a high volume of incoming requests. As a result, the initial review may take up to 10-15 business days. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates the window-layouts Raycast extension by adding more built-in layouts and introducing “smart”/custom workflows (auto layout, picking layout with window reordering, saving/restoring, custom layouts), alongside refactors to centralize desktop/window discovery and enable layout reuse.
Changes:
- Added new layouts (75/25 splits, 3x3, 6-grid, centered focus, PiP) and corresponding no-view commands.
- Introduced new view commands: pick layout (with ordering), auto layout, save/restore layouts, and create/browse custom layouts.
- Refactored layout utilities (desktop context, preference parsing, exported helpers) and updated docs/changelog.
Reviewed changes
Copilot reviewed 27 out of 41 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| extensions/window-layouts/src/vertical-75-25.ts | Adds a new command that applies the vertical 75/25 layout. |
| extensions/window-layouts/src/vertical-25-75.ts | Adds a new command that applies the vertical 25/75 layout. |
| extensions/window-layouts/src/utils/saved-layouts.ts | Adds LocalStorage persistence helpers for saved window placements. |
| extensions/window-layouts/src/utils/layout/window-layouts.ts | Adds multiple new preset layout definitions (grids, splits, special). |
| extensions/window-layouts/src/utils/layout/index.ts | Re-exports new layouts and newly exported layout helper functions. |
| extensions/window-layouts/src/utils/layout/create-layout.ts | Refactors layout application around DesktopContext; exports helpers; improves failure reporting. |
| extensions/window-layouts/src/utils/index.ts | Updates exports to include DesktopContext + layout helper exports. |
| extensions/window-layouts/src/utils/get-user-preferences.ts | Makes preferences access synchronous; adds excludedApps parsing. |
| extensions/window-layouts/src/utils/get-resizable-windows.ts | Removes old window discovery helper (superseded by getDesktopContext). |
| extensions/window-layouts/src/utils/get-desktop-context.ts | Adds unified desktop + filtered window discovery (including excluded apps). |
| extensions/window-layouts/src/utils/get-active-desktop.ts | Removes old desktop discovery helper (superseded by getDesktopContext). |
| extensions/window-layouts/src/utils/custom-layouts.ts | Adds LocalStorage persistence helpers for user-defined JSON grid layouts. |
| extensions/window-layouts/src/save-layout.tsx | Adds UI to save current window positions to LocalStorage. |
| extensions/window-layouts/src/restore-layout.tsx | Adds UI to browse and restore saved layouts by app-name matching. |
| extensions/window-layouts/src/pip.ts | Adds a new command that applies the PiP layout. |
| extensions/window-layouts/src/pick-layout.tsx | Adds UI to reorder windows and apply a chosen layout by slot order. |
| extensions/window-layouts/src/horizontal-75-25.ts | Adds a new command that applies the horizontal 75/25 layout. |
| extensions/window-layouts/src/horizontal-25-75.ts | Adds a new command that applies the horizontal 25/75 layout. |
| extensions/window-layouts/src/grid-6.ts | Adds a new command that applies the 3x2 grid layout. |
| extensions/window-layouts/src/grid-3x3.ts | Adds a new command that applies the 3x3 grid layout. |
| extensions/window-layouts/src/custom-layouts.tsx | Adds UI to list/apply/delete custom layouts. |
| extensions/window-layouts/src/create-custom-layout.tsx | Adds UI to create and validate custom layouts from JSON. |
| extensions/window-layouts/src/centered-focus.ts | Adds a new command that applies the centered focus layout. |
| extensions/window-layouts/src/auto-layout.ts | Adds a command that chooses a layout based on window count. |
| extensions/window-layouts/package.json | Registers new commands/preferences and fixes a typo in an existing description. |
| extensions/window-layouts/README.md | Updates documentation to cover new layouts, commands, and preferences. |
| extensions/window-layouts/CHANGELOG.md | Documents newly added features/refactors and bug fixes. |
Greptile SummaryThis PR significantly expands the window-layouts extension with eight new layout commands (75/25 splits, grids of 6 and 9, Centered Focus, Picture-in-Picture), six new view-type commands (Auto Layout, Pick Layout, Save/Restore Layout, Create/Browse Custom Layouts), an Confidence Score: 5/5Safe to merge — all findings are P2 style/consistency suggestions with no blocking issues. The core logic is sound: the unified getDesktopContext helper, failure-count reporting, and new layout definitions all look correct. Both open findings are P2: a manually defined Preferences type (should use the auto-generated one) and a missing popToRoot() in save-layout.tsx. extensions/window-layouts/src/utils/get-user-preferences.ts and extensions/window-layouts/src/save-layout.tsx have minor style issues worth addressing.
|
| Filename | Overview |
|---|---|
| extensions/window-layouts/src/utils/get-user-preferences.ts | Refactored to synchronous; correctly fixes NaN gap fallback; but manually defines UserPreferences type instead of using the auto-generated Preferences from raycast-env.d.ts |
| extensions/window-layouts/src/save-layout.tsx | New command to save window positions; missing popToRoot() after successful save unlike the sibling create-custom-layout command |
| extensions/window-layouts/src/utils/get-desktop-context.ts | New unified helper replacing getActiveDesktop + getResizableWindows; correctly awaits toast calls, parallelises API calls, and applies excludedApps filter |
| extensions/window-layouts/src/utils/layout/create-layout.ts | Exported calculateCellSize/getWindowFrames; applyLayout now returns failure count and animated toast is hidden on early returns; getUserPreferences correctly made synchronous |
| extensions/window-layouts/src/pick-layout.tsx | New view command to reorder windows before applying a layout; logic is sound |
| extensions/window-layouts/src/restore-layout.tsx | New command to restore saved layouts; correctly handles multiple windows from the same app with a usedWindowIds set |
| extensions/window-layouts/src/create-custom-layout.tsx | New view command for JSON-based custom layout creation; validation and popToRoot on success are correct |
| extensions/window-layouts/src/auto-layout.ts | New no-view command that maps window count to an appropriate layout; falls back to GRID_3X3 for 10+ windows |
| extensions/window-layouts/package.json | New commands and excludedApps preference added; titles use Title Case; typo in vertical-50-50 description fixed |
| extensions/window-layouts/CHANGELOG.md | New entry correctly uses {PR_MERGE_DATE} placeholder and is placed at the top of the file |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/window-layouts/src/utils/get-user-preferences.ts
Line: 3-9
Comment:
**Manually defined Preferences type**
`UserPreferences` is a hand-written type for `getPreferenceValues()`, which is flagged by the project's rules. Raycast auto-generates the `Preferences` interface in `raycast-env.d.ts` when the extension runs, so a manual definition can drift out of sync with `package.json`. Use the generated type instead:
```suggestion
import { getPreferenceValues } from "@raycast/api";
export function getUserPreferences() {
const userPreferences = getPreferenceValues<Preferences>();
const gap = parseInt(userPreferences.gap as string, 10);
const rawExcluded = (userPreferences.excludedApps as string) ?? "";
```
**Rule Used:** What: Don't manually define `Preferences` for `get... ([source](https://app.greptile.com/review/custom-context?memory=d93fc9fb-a45d-4479-a6a4-b1b4af98ebc8))
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/window-layouts/src/save-layout.tsx
Line: 33-42
Comment:
**Missing `popToRoot()` after successful save**
`create-custom-layout.tsx` calls `popToRoot()` after a successful submit, but `save-layout.tsx` does not — leaving the form open after the success toast. Consider adding it for consistency:
```suggestion
await saveLayout({
name,
windows,
savedAt: new Date().toISOString(),
});
await showToast({
style: Toast.Style.Success,
title: `Layout "${name}" saved`,
message: `${windows.length} window(s)`,
});
await popToRoot();
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Update window-layouts extension" | Re-trigger Greptile
- Use auto-generated Preferences type instead of manual UserPreferences - Add popToRoot() after successful save in save-layout - Use structural type guard for w.bounds instead of forced cast - Respect disableToasts preference in pick-layout - Update catch block in create-layout to reuse toast instead of creating new one - Guard gridPreview against empty/invalid grids in custom-layouts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Merge branch \'contributions/merge-1775742411252\' - Pull contributions - fix: address PR review feedback and compress metadata images
|
Hi @gfazioli ! Thank you for contributing, pretty cool features it seems 🙌 Is this ready for review or are you still working on it? |
Hi @teemusuvinen! Thank you 🙏 Yes, it's ready for review — I'm not actively working on it anymore. All the review feedback from Copilot and Greptile has already been addressed in the latest commits. Looking forward to your review! |
| async function handleDelete(layout: CustomLayout) { | ||
| await deleteCustomLayout(layout.name); | ||
| await loadLayouts(); | ||
| await showToast({ style: Toast.Style.Success, title: `Deleted "${layout.name}"` }); | ||
| } |
There was a problem hiding this comment.
We could add a confirm dialog before deleting a layout to avoid accidental deletions:
| async function handleDelete(layout: CustomLayout) { | |
| await deleteCustomLayout(layout.name); | |
| await loadLayouts(); | |
| await showToast({ style: Toast.Style.Success, title: `Deleted "${layout.name}"` }); | |
| } | |
| async function handleDelete(layout: CustomLayout) { | |
| const isConfirmed = await confirmAlert({ | |
| title: `Delete "${layout.name}"?`, | |
| primaryAction: { title: "Delete", style: Alert.ActionStyle.Destructive }, | |
| dismissAction: { title: "Cancel" }, | |
| // rememberUserChoice: true <-- optionally with "Do not show this message again" checkbox | |
| }); | |
| if (!isConfirmed) return; | |
| await deleteCustomLayout(layout.name); | |
| await loadLayouts(); | |
| await showToast({ style: Toast.Style.Success, title: `Deleted "${layout.name}"` }); | |
| } |
| {layouts.length === 0 && !isLoading ? ( | ||
| <List.EmptyView | ||
| title="No Custom Layouts" | ||
| description="Use 'Create Custom Layout' to define your own grid layouts." |
There was a problem hiding this comment.
Here could be an action to easily navigate to the "Create Custom Layout" command:
async function handleCreate() {
// this will pop back to custom layouts list after a new layout has been created
pop();
await loadLayouts();
}
.....
actions={
<ActionPanel>
<Action.Push
target={<CreateCustomLayout onCreate={handleCreate} />}
title="Create Custom Layout"
icon={Icon.Plus}
shortcut={Keyboard.Shortcut.Common.New}
/>
</ActionPanel>
}
.....
// create-custom-layout.tsx
type CreateCustomLayoutProps = {
onCreate?: () => void;
};
export default function Command({ onCreate }: CreateCustomLayoutProps) {
.....
async function handleSubmit(values: { name: string; grid: string }) {
....
await showToast({
style: Toast.Style.Success,
title: `Layout "${name}" created`,
});
if (onCreate) {
onCreate();
return;
}
await popToRoot();
}| title="Delete Layout" | ||
| icon={Icon.Trash} | ||
| style={Action.Style.Destructive} | ||
| shortcut={{ modifiers: ["cmd"], key: "d" }} |
There was a problem hiding this comment.
This is probably personal preference but my muscle-memory for deleting things in Raycast would want to use the common delete shortcut ctrl+x 😄
// import { Keyboard } from "@raycast/api"
shortcut={Keyboard.Shortcut.Common.Remove}| layouts[existing] = layout; | ||
| } else { |
There was a problem hiding this comment.
We could add a confirm dialog here to prevent accidental overwrites to existing layouts
| layouts[existing] = layout; | |
| } else { | |
| const isConfirmed = await confirmAlert({ | |
| title: `A layout named "${layout.name}" already exists. Do you want to overwrite it?`, | |
| primaryAction: { title: "Overwrite", style: Alert.ActionStyle.Destructive }, | |
| dismissAction: { title: "Cancel" }, | |
| // rememberUserChoice: true <-- optionally with "Do not show this message again" checkbox | |
| }); | |
| if (!isConfirmed) return; | |
| layouts[existing] = layout; | |
| } else { |
| <Action | ||
| title="Move Down" | ||
| icon={Icon.ArrowDown} | ||
| shortcut={{ modifiers: ["cmd"], key: "arrowDown" }} |
There was a problem hiding this comment.
cmd+arrowUp and cmd+arrowDown don't seem to work on my keyboard - it just jumps between the List.Section elements
| <List.Item | ||
| key={w.id} | ||
| title={`${i + 1}. ${w.application?.name ?? "Unknown"}`} | ||
| icon={Icon.Window} |
| const AVAILABLE_LAYOUTS: NamedLayout[] = [ | ||
| { name: "Horizontal 50/50", layout: layouts.HORIZONTAL_50_50, slots: 2 }, | ||
| { name: "Horizontal 70/30", layout: layouts.HORIZONTAL_70_30, slots: 2 }, | ||
| { name: "Horizontal 30/70", layout: layouts.HORIZONTAL_30_70, slots: 2 }, | ||
| { name: "Horizontal 75/25", layout: layouts.HORIZONTAL_75_25, slots: 2 }, | ||
| { name: "Horizontal 25/75", layout: layouts.HORIZONTAL_25_75, slots: 2 }, | ||
| { name: "Horizontal 3 Columns", layout: layouts.HORIZONTAL_3, slots: 3 }, | ||
| { name: "Horizontal 1+2", layout: layouts.HORIZONTAL_1_2, slots: 3 }, | ||
| { name: "Horizontal 2+1", layout: layouts.HORIZONTAL_2_1, slots: 3 }, | ||
| { name: "Vertical 50/50", layout: layouts.VERTICAL_50_50, slots: 2 }, | ||
| { name: "Vertical 70/30", layout: layouts.VERTICAL_70_30, slots: 2 }, | ||
| { name: "Vertical 30/70", layout: layouts.VERTICAL_30_70, slots: 2 }, | ||
| { name: "Vertical 75/25", layout: layouts.VERTICAL_75_25, slots: 2 }, | ||
| { name: "Vertical 25/75", layout: layouts.VERTICAL_25_75, slots: 2 }, | ||
| { name: "Vertical 3 Rows", layout: layouts.VERTICAL_3, slots: 3 }, | ||
| { name: "Vertical 1+2", layout: layouts.VERTICAL_1_2, slots: 3 }, | ||
| { name: "Vertical 2+1", layout: layouts.VERTICAL_2_1, slots: 3 }, | ||
| { name: "Grid of 4", layout: layouts.GRID, slots: 4 }, | ||
| { name: "Grid of 6", layout: layouts.GRID_6, slots: 6 }, | ||
| { name: "Grid of 9", layout: layouts.GRID_3X3, slots: 9 }, | ||
| { name: "Centered Focus", layout: layouts.CENTERED_FOCUS, slots: 3 }, | ||
| { name: "Picture in Picture", layout: layouts.PIP, slots: 2 }, | ||
| ]; |
There was a problem hiding this comment.
If we add the actual layout icons here:
| const AVAILABLE_LAYOUTS: NamedLayout[] = [ | |
| { name: "Horizontal 50/50", layout: layouts.HORIZONTAL_50_50, slots: 2 }, | |
| { name: "Horizontal 70/30", layout: layouts.HORIZONTAL_70_30, slots: 2 }, | |
| { name: "Horizontal 30/70", layout: layouts.HORIZONTAL_30_70, slots: 2 }, | |
| { name: "Horizontal 75/25", layout: layouts.HORIZONTAL_75_25, slots: 2 }, | |
| { name: "Horizontal 25/75", layout: layouts.HORIZONTAL_25_75, slots: 2 }, | |
| { name: "Horizontal 3 Columns", layout: layouts.HORIZONTAL_3, slots: 3 }, | |
| { name: "Horizontal 1+2", layout: layouts.HORIZONTAL_1_2, slots: 3 }, | |
| { name: "Horizontal 2+1", layout: layouts.HORIZONTAL_2_1, slots: 3 }, | |
| { name: "Vertical 50/50", layout: layouts.VERTICAL_50_50, slots: 2 }, | |
| { name: "Vertical 70/30", layout: layouts.VERTICAL_70_30, slots: 2 }, | |
| { name: "Vertical 30/70", layout: layouts.VERTICAL_30_70, slots: 2 }, | |
| { name: "Vertical 75/25", layout: layouts.VERTICAL_75_25, slots: 2 }, | |
| { name: "Vertical 25/75", layout: layouts.VERTICAL_25_75, slots: 2 }, | |
| { name: "Vertical 3 Rows", layout: layouts.VERTICAL_3, slots: 3 }, | |
| { name: "Vertical 1+2", layout: layouts.VERTICAL_1_2, slots: 3 }, | |
| { name: "Vertical 2+1", layout: layouts.VERTICAL_2_1, slots: 3 }, | |
| { name: "Grid of 4", layout: layouts.GRID, slots: 4 }, | |
| { name: "Grid of 6", layout: layouts.GRID_6, slots: 6 }, | |
| { name: "Grid of 9", layout: layouts.GRID_3X3, slots: 9 }, | |
| { name: "Centered Focus", layout: layouts.CENTERED_FOCUS, slots: 3 }, | |
| { name: "Picture in Picture", layout: layouts.PIP, slots: 2 }, | |
| ]; | |
| const AVAILABLE_LAYOUTS: NamedLayout[] = [ | |
| { icon: "horizontal-50-50.png", name: "Horizontal 50/50", layout: layouts.HORIZONTAL_50_50, slots: 2 }, | |
| { icon: "horizontal-70-30.png", name: "Horizontal 70/30", layout: layouts.HORIZONTAL_70_30, slots: 2 }, | |
| { icon: "horizontal-30-70.png", name: "Horizontal 30/70", layout: layouts.HORIZONTAL_30_70, slots: 2 }, | |
| { icon: "horizontal-75-25.png", name: "Horizontal 75/25", layout: layouts.HORIZONTAL_75_25, slots: 2 }, | |
| { icon: "horizontal-25-75.png", name: "Horizontal 25/75", layout: layouts.HORIZONTAL_25_75, slots: 2 }, | |
| { icon: "horizontal-3.png", name: "Horizontal 3 Columns", layout: layouts.HORIZONTAL_3, slots: 3 }, | |
| { icon: "horizontal-1-2.png", name: "Horizontal 1+2", layout: layouts.HORIZONTAL_1_2, slots: 3 }, | |
| { icon: "horizontal-2-1.png", name: "Horizontal 2+1", layout: layouts.HORIZONTAL_2_1, slots: 3 }, | |
| { icon: "vertical-50-50.png", name: "Vertical 50/50", layout: layouts.VERTICAL_50_50, slots: 2 }, | |
| { icon: "vertical-70-30.png", name: "Vertical 70/30", layout: layouts.VERTICAL_70_30, slots: 2 }, | |
| { icon: "vertical-30-70.png", name: "Vertical 30/70", layout: layouts.VERTICAL_30_70, slots: 2 }, | |
| { icon: "vertical-75-25.png", name: "Vertical 75/25", layout: layouts.VERTICAL_75_25, slots: 2 }, | |
| { icon: "vertical-25-75.png", name: "Vertical 25/75", layout: layouts.VERTICAL_25_75, slots: 2 }, | |
| { icon: "vertical-3.png", name: "Vertical 3 Rows", layout: layouts.VERTICAL_3, slots: 3 }, | |
| { icon: "vertical-1-2.png", name: "Vertical 1+2", layout: layouts.VERTICAL_1_2, slots: 3 }, | |
| { icon: "vertical-2-1.png", name: "Vertical 2+1", layout: layouts.VERTICAL_2_1, slots: 3 }, | |
| { icon: "grid.png", name: "Grid of 4", layout: layouts.GRID, slots: 4 }, | |
| { icon: "grid-6.png", name: "Grid of 6", layout: layouts.GRID_6, slots: 6 }, | |
| { icon: "grid-3x3.png", name: "Grid of 9", layout: layouts.GRID_3X3, slots: 9 }, | |
| { icon: "centered-focus.png", name: "Centered Focus", layout: layouts.CENTERED_FOCUS, slots: 3 }, | |
| { icon: "pip.png", name: "Picture in Picture", layout: layouts.PIP, slots: 2 }, | |
| ]; |
We can then show the actual layout icon here instead of a generic grid icon
{matchingLayouts.map((nl) => (
<List.Item
....
// Actual icon here instead of generic 2x2 grid 🙂
icon={{ source: `icons/${nl.icon}` }}
| await deleteSavedLayout(layout.name); | ||
| await loadLayouts(); | ||
| await showToast({ style: Toast.Style.Success, title: `Deleted "${layout.name}"` }); | ||
| } |
There was a problem hiding this comment.
We could show a confirm dialog here to avoid accidental deletions
async function handleDelete(layout: SavedLayout) {
const isConfirmed = await confirmAlert({
title: `Delete "${layout.name}"?`,
primaryAction: { title: "Delete", style: Alert.ActionStyle.Destructive },
dismissAction: { title: "Cancel" },
// rememberUserChoice: true <-- optionally with "Do not show this message again" checkbox
});
if (!isConfirmed) return;
await deleteCustomLayout(layout.name);
await loadLayouts();
await showToast({ style: Toast.Style.Success, title: `Deleted "${layout.name}"` });
}| export async function saveCustomLayout(layout: CustomLayout): Promise<void> { | ||
| const all = await getCustomLayouts(); | ||
| const existing = all.findIndex((l) => l.name === layout.name); | ||
| if (existing >= 0) { |
There was a problem hiding this comment.
We could add a confirm dialog here to prevent accidental overwrites to existing layouts
const isConfirmed = await confirmAlert({
title: `A layout named "${layout.name}" already exists. Do you want to overwrite it?`,
primaryAction: { title: "Overwrite", style: Alert.ActionStyle.Destructive },
dismissAction: { title: "Cancel" },
// rememberUserChoice: true <-- optionally with "Do not show this message again" checkbox
});
if (!isConfirmed) return;
all[existing] = layout;|
@gfazioli I have now checked the PR and added mostly suggestions to enhance the functionality but overall super nice work and useful additions! 🙏 Feel free to also send me a message in Raycast Slack if you want to discuss something in more detail 🙂 |

Description
Screencast
Checklist
npm run buildand tested this distribution build in Raycastassetsfolder are used by the extension itselfREADMEare placed outside of themetadatafolder