Zipic: rename, AI Extension, and unified presets#27650
Zipic: rename, AI Extension, and unified presets#27650okooo5km wants to merge 6 commits intoraycast:mainfrom
Conversation
- Rename extension folder from `images-compression` to `zipic` to match
the brand and shorten AI mention from `@images-compression` to `@zipic`.
- Add `Compress Images with Options` for one-off compression with custom
settings (level, format, resize, save location, suffix, subfolder).
- Add `Compress Images with Preset` that reads compression presets directly
from the Zipic app (UserDefaults), establishing a single source of truth
for presets between the app and the extension.
- Add AI Extension with three tools:
* `optimize-images` — full-control compression (16 URL params)
* `list-presets` — read user's saved Zipic presets
* `compress-with-preset` — apply a saved preset by name or ID
- Replace post-success `showToast` + `popToRoot` with `showHUD` for a
reliable independent on-screen confirmation that survives window close.
- Upgrade dependencies to latest Raycast API (1.104.x), `@raycast/utils` 2.x,
TypeScript 5.9, Prettier 3.8, etc.
- Add `platforms: ["macOS"]`.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR rebrands the Raycast extension from images-compression to zipic and adds new Zipic-powered capabilities, including AI tools and preset-driven compression sourced from the Zipic app.
Changes:
- Renames the extension to
zipicand updates associated metadata/docs. - Adds new commands (options + preset) and AI tools (
optimize-images,list-presets,compress-with-preset). - Implements reading Zipic presets via macOS
defaultsand builds Zipic URL-scheme calls accordingly.
Reviewed changes
Copilot reviewed 17 out of 27 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| extensions/zipic/package.json | New extension manifest for Zipic rename, commands, and AI tool registration |
| extensions/zipic/src/utils/checkInstall.ts | Improves install detection and updates install URL |
| extensions/zipic/src/utils/zipicPresets.ts | Reads presets from Zipic UserDefaults and builds Zipic compression URLs |
| extensions/zipic/src/tools/optimize-images.ts | Adds AI tool for compression with explicit options + remote URL download |
| extensions/zipic/src/tools/list-presets.ts | Adds AI tool to list presets from Zipic |
| extensions/zipic/src/tools/compress-with-preset.ts | Adds AI tool to compress using a named/ID preset |
| extensions/zipic/src/compressImages.tsx | New no-view command using Zipic URL scheme |
| extensions/zipic/src/compressImagesWithOptions.tsx | New interactive command to configure compression options |
| extensions/zipic/src/compressImagesWithPreset.tsx | New interactive preset picker command |
| extensions/zipic/README.md | New documentation for Zipic extension |
| extensions/zipic/CHANGELOG.md | Changelog entry for rename + AI + presets |
| extensions/zipic/tsconfig.json | Adds a new d.ts include |
| extensions/zipic/.gitignore | Updates ignored files (adds .cursor, removes swift build artifacts) |
| extensions/images-compression/* | Removes old extension implementation and metadata |
| const url = `zipic://compress?${urlParams}`; | ||
|
|
||
| try { | ||
| // Execute compression command | ||
| exec(`open "${url}"`); | ||
| } catch (error) { |
| const localPath = path.join(desktopDir, filename); | ||
|
|
||
| // Create write stream | ||
| const file = fs.createWriteStream(localPath); | ||
|
|
||
| // Choose http or https based on URL | ||
| const requester = url.startsWith("https") ? https : http; | ||
|
|
||
| // Download the file | ||
| requester | ||
| .get(url, (response) => { | ||
| // Check if redirection | ||
| if (response.statusCode === 301 || response.statusCode === 302) { | ||
| if (response.headers.location) { | ||
| downloadFile(response.headers.location).then(resolve); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| // Check for successful response | ||
| if (response.statusCode !== 200) { | ||
| resolve(null); | ||
| return; | ||
| } | ||
|
|
||
| // Pipe the response to the file | ||
| response.pipe(file); |
| // If adding subfolder | ||
| if (addSubfolder) { | ||
| outputDir = path.join(outputDir, "compressed"); | ||
| } |
| "icon": "zipic-icon.png", | ||
| "options": [ | ||
| { | ||
| "name": "directory", | ||
| "title": "Custom Directory", | ||
| "description": "Path to save the compressed files (only used when Location is set to Custom and Use Default Save Directory is disabled)", | ||
| "type": "directory", | ||
| "required": false, | ||
| "dependsOn": [ | ||
| "location", | ||
| "specified" | ||
| ], | ||
| "dependsOnValue": [ | ||
| "custom", | ||
| false | ||
| ] | ||
| } | ||
| ] |
| ### Manage Presets | ||
|
|
||
| Create, edit, and manage your compression presets. |
| ### 3. Presets | ||
|
|
||
| Use the `Manage Compression Presets` command to create and manage presets: | ||
|
|
||
| - **Create Presets**: Save your frequently used compression settings as named presets | ||
| - **Edit Presets**: Modify existing presets as needed | ||
| - **Set Default Preset**: Choose a preset to be used by the `Compress Images` command |
| async function isZipicInstalled() { | ||
| const applications = await getApplications(); | ||
| return applications.some(({ bundleId }) => bundleId === "studio.5km.zipic"); | ||
| try { | ||
| const applications = await getApplications(); | ||
| return applications.some(({ bundleId }) => bundleId === "studio.5km.zipic"); | ||
| } catch (error) { | ||
| return false; | ||
| } | ||
| } |
| const { presets, selectedPresetId } = await readZipicPresets(); | ||
|
|
||
| return { | ||
| success: true, | ||
| count: presets.length, | ||
| selectedPresetId, | ||
| presets: presets.map((p) => ({ | ||
| id: p.id, | ||
| name: p.name, | ||
| isDefault: p.isDefault, | ||
| isFavorite: p.isFavorite, | ||
| summary: describePreset(p), | ||
| level: p.compressionOption.level, | ||
| format: p.compressionOption.format, | ||
| })), | ||
| }; |
| validPaths.map(async (path) => { | ||
| if (path.startsWith("http://") || path.startsWith("https://")) { | ||
| const localPath = await downloadFile(path); | ||
| return localPath || path; // If download fails, keep original URL | ||
| } | ||
| // Resolve tilde in path if present | ||
| if (path.startsWith("~")) { | ||
| return path.replace(/^~/, homedir()); | ||
| } | ||
| return path; |
Greptile SummaryThis PR renames the Confidence Score: 5/5The extension is safe to merge; all changes are additive and well-scoped with solid error handling in the view commands. Key concerns from earlier rounds (redirect depth limit, write stream leak, unused npm package, zero-dimension URL params, missing try/catch around open()) have all been addressed. Remaining feedback is non-blocking: a missing subfolder-name field in the options form and a hardcoded path segment in the informational expected-output prediction. extensions/zipic/src/compressImagesWithOptions.tsx (missing subfolder name input) and extensions/zipic/src/tools/optimize-images.ts (hardcoded subfolder name in expected-path calculation) Important Files Changed
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
extensions/zipic/src/compressImagesWithOptions.tsx:285-291
When "Add Subfolder" is enabled the form only appends `addSubfolder=true` to the URL with no subfolder name. The parallel AI tool (`optimize-images`) sends an explicit `subfolder` value (defaulting to `"zipic"`). If Zipic's URL scheme doesn't supply a fallback, the compressed files land in an unnamed or empty-string subfolder — inconsistent with every other call path in this extension.
```suggestion
<Form.Checkbox
id="addSubfolder"
title="Add Subfolder"
label="Add Subfolder"
value={formValues.addSubfolder}
onChange={(value) => setFormValues({ ...formValues, addSubfolder: value })}
/>
{formValues.addSubfolder && (
<Form.TextField
id="subfolder"
title="Subfolder Name"
placeholder="zipic"
info="Name of the subfolder to save compressed files in"
value={formValues.subfolder ?? "zipic"}
onChange={(value) => setFormValues({ ...formValues, subfolder: value })}
/>
)}
```
### Issue 2 of 3
extensions/zipic/src/tools/optimize-images.ts:407-415
The `expectedOutputPaths` prediction appends a hardcoded `"compressed"` directory name when `addSubfolder` is true, but the actual Zipic URL is built with the caller-supplied `subfolder` param (defaulting to `"zipic"`). The predicted paths will be wrong whenever `subfolder !== "compressed"`, which is the common case.
```suggestion
// If adding subfolder
if (addSubfolder) {
outputDir = path.join(outputDir, subfolder);
}
return outputDir;
} else {
// Handle files as before
const dir = path.dirname(resolvedPath);
```
### Issue 3 of 3
extensions/zipic/src/tools/optimize-images.ts:430-436
Same hardcoded `"compressed"` issue as in the directory branch above — uses the literal string instead of the `subfolder` param for individual file predictions.
```suggestion
// If adding subfolder
if (addSubfolder) {
outputDir = path.join(outputDir, subfolder);
}
// Determine filename and extension
let fileName = baseName;
```
Reviews (6): Last reviewed commit: "compress-with-preset: guard open() with ..." | Re-trigger Greptile |
- optimize-images: replace fire-and-forget exec with await open(url) so failures from the URL scheme propagate as the tool's error result. - optimize-images: bound downloadFile recursion with a maxRedirects counter (default 5) to avoid stack overflow on redirect loops. - compressImagesWithOptions: drop the unused getPreferenceValues call and the manual CompressionOptions interface (the command has no preferences declared in the manifest, so all reads were always undefined). Use a static DEFAULT_FORM_VALUES constant instead. - Remove the unused `open` npm dependency (all URL opening goes through `open` from @raycast/api).
|
👋 Heads up on the failing Check workflow: it's trying to The other CI jobs (NPM Check on both Could you advise on the right way to handle this? A couple of options I can think of:
Happy to take whichever path you prefer. The local |
Greptile flagged that fs.createWriteStream() was called eagerly before the HTTP status code was inspected. On 301/302 redirects and non-200 responses the early `return` skipped past the only `file.close()` call, leaving an empty file on disk and an open file descriptor for every redirect hop. Defer creating the write stream until a successful 200 response. Drain the response body on the early-exit branches, and only delete a partial file when the stream itself errors after pipe.
|
|
||
| if (!presetMatch) { | ||
| return { presets: [], selectedPresetId: null }; | ||
| } | ||
|
|
||
| const base64 = presetMatch[1].replace(/\s+/g, ""); | ||
| const json = Buffer.from(base64, "base64").toString("utf-8"); | ||
| const presets = JSON.parse(json) as ZipicPreset[]; | ||
|
|
||
| return { | ||
| presets, | ||
| selectedPresetId: selectedMatch ? selectedMatch[1] : null, | ||
| }; | ||
| } | ||
|
|
||
| export function buildCompressURL(filePaths: string[], preset: ZipicPreset): string { | ||
| const params = new URLSearchParams(); |
There was a problem hiding this comment.
Unhandled throw when Zipic defaults domain is absent
readZipicPresets calls execFileAsync("defaults", ["export", ZIPIC_BUNDLE_ID, "-"]), which exits with code 1 and throws when the domain has never been written to the macOS UserDefaults store (i.e., Zipic is installed but has never been launched). The calling AI tools — list-presets.ts and compress-with-preset.ts — have no try/catch around this call, so the error propagates as an unhandled rejection to the Raycast AI layer instead of a friendly structured response. The view command (compressImagesWithPreset.tsx) correctly wraps the call in a try/catch, but the tool equivalents do not.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zipic/src/utils/zipicPresets.ts
Line: 44-60
Comment:
**Unhandled throw when Zipic defaults domain is absent**
`readZipicPresets` calls `execFileAsync("defaults", ["export", ZIPIC_BUNDLE_ID, "-"])`, which exits with code 1 and throws when the domain has never been written to the macOS UserDefaults store (i.e., Zipic is installed but has never been launched). The calling AI tools — `list-presets.ts` and `compress-with-preset.ts` — have no try/catch around this call, so the error propagates as an unhandled rejection to the Raycast AI layer instead of a friendly structured response. The view command (`compressImagesWithPreset.tsx`) correctly wraps the call in a try/catch, but the tool equivalents do not.
How can I resolve this? If you propose a fix, please make it concise.| const { presets, selectedPresetId } = await readZipicPresets(); | ||
|
|
||
| return { |
There was a problem hiding this comment.
Wrap
readZipicPresets() in a try/catch so a missing or corrupt Zipic defaults domain (e.g. app installed but never launched) returns a structured error instead of an unhandled rejection to the AI layer.
| const { presets, selectedPresetId } = await readZipicPresets(); | |
| return { | |
| let presets, selectedPresetId; | |
| try { | |
| ({ presets, selectedPresetId } = await readZipicPresets()); | |
| } catch { | |
| return { success: false, error: "Could not read Zipic preferences. Try launching Zipic at least once." }; | |
| } | |
| return { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zipic/src/tools/list-presets.ts
Line: 15-17
Comment:
Wrap `readZipicPresets()` in a try/catch so a missing or corrupt Zipic defaults domain (e.g. app installed but never launched) returns a structured error instead of an unhandled rejection to the AI layer.
```suggestion
let presets, selectedPresetId;
try {
({ presets, selectedPresetId } = await readZipicPresets());
} catch {
return { success: false, error: "Could not read Zipic preferences. Try launching Zipic at least once." };
}
return {
```
How can I resolve this? If you propose a fix, please make it concise.| const { presets, selectedPresetId } = await readZipicPresets(); | ||
| if (presets.length === 0) { |
There was a problem hiding this comment.
Same as
list-presets.ts: readZipicPresets() throws when the Zipic defaults domain doesn't exist, and there is no catch here to convert that into a structured return value.
| const { presets, selectedPresetId } = await readZipicPresets(); | |
| if (presets.length === 0) { | |
| let presets, selectedPresetId; | |
| try { | |
| ({ presets, selectedPresetId } = await readZipicPresets()); | |
| } catch { | |
| return { success: false, error: "Could not read Zipic preferences. Try launching Zipic at least once." }; | |
| } | |
| if (presets.length === 0) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zipic/src/tools/compress-with-preset.ts
Line: 32-33
Comment:
Same as `list-presets.ts`: `readZipicPresets()` throws when the Zipic defaults domain doesn't exist, and there is no catch here to convert that into a structured return value.
```suggestion
let presets, selectedPresetId;
try {
({ presets, selectedPresetId } = await readZipicPresets());
} catch {
return { success: false, error: "Could not read Zipic preferences. Try launching Zipic at least once." };
}
if (presets.length === 0) {
```
How can I resolve this? If you propose a fix, please make it concise.`URLSearchParams.toString()` encodes spaces as `+` (form-urlencoded convention), but Zipic parses URL queries via Swift's `URLComponents. queryItems`, which does not decode `+` back to space. Files such as `~/Downloads/My Photo.png` were therefore handed to Zipic as `~/Downloads/My+Photo.png` and could not be found. Replace `+` with `%20` after `toString()`. Literal `+` characters in file paths are encoded as `%2B` by URLSearchParams, so the replace is safe (no false positives). Affected commands: - `Compress Images` - `Compress Images with Preset` (via buildCompressURL)
Align preset URL building with the other call sites (`optimize-images` and `compressImagesWithOptions`) which only append `width`/`height` when the value is greater than zero. Sending `width=0&height=0` for presets without resize configuration could cause Zipic to interpret the request as a literal resize to zero dimensions.
Match the pattern used elsewhere in the extension. If the URL scheme is rejected or Zipic isn't launchable, return a structured error to the AI layer instead of letting the rejection escape as an unhandled error.
Description
Major update to the existing Images Compression extension. Three things in one PR — they're tightly coupled so it didn't make sense to split.
1. Rename:
images-compression→zipicThe original name violated the Store guideline against generic names ("Images Compression" is not a Title Case landmark and conflicts with several similar-sounding extensions). The rename:
@images-compression(17 chars) to@zipic(6 chars)name, andtitleare all updated consistently2. New Features
Commands
Compress Images— unchangedCompress Images with Options— full-form compression with explicit options: level, format, resize, save location, suffix, subfolder, default-save-directory toggleCompress Images with Preset— reads compression presets directly from the Zipic app'sUserDefaults(studio.5km.zipic, keyPresetData) and applies them via the URL scheme. Establishes a single source of truth for presets — Raycast can read them but not edit them (managing presets stays in the Zipic app)AI Extension (
@zipic)Three tools:
optimize-images— full-control compression. Covers all 16 parameters supported by Zipic's URL scheme (level,format,width,height,ratio,location,directory,specified,addSuffix,suffix,addSubfolder,subfolder,overwrite,progressive, plus remote URL download)list-presets— read the user's saved Zipic presetscompress-with-preset— apply a saved preset by name or ID (case-insensitive, with substring fallback)12 evals (3 consecutive runs at 100% / 12 passing), including a negative assertion guarding "what can you do?" from accidentally triggering compression.
3. Maintenance
@raycast/api1.93 → 1.104,@raycast/utils1.x → 2.xshowToast + popToRootwithshowHUDso confirmations show as an independent on-screen capsule that survives window closeconsole.errortoshowFailureToastfrom@raycast/utils"platforms": ["macOS"]Checklist
npm run lintpassesnpm run buildpassesnpx ray evalspasses (12/12 across 3 consecutive runs)npm run dev{PR_MERGE_DATE}placeholderA note on the rename
I'm the original author of the extension, and I considered keeping the slug
images-compression. Two reasons I chose to rename:@-mention syntax forces the slug into user-facing UX.@images-compressionis awkward and discouraging.Happy to discuss if a redirect or any migration concern needs handling on the Store side.