Skip to content

Zipic: rename, AI Extension, and unified presets#27650

Open
okooo5km wants to merge 6 commits intoraycast:mainfrom
okooo5km:update-images-compression
Open

Zipic: rename, AI Extension, and unified presets#27650
okooo5km wants to merge 6 commits intoraycast:mainfrom
okooo5km:update-images-compression

Conversation

@okooo5km
Copy link
Copy Markdown
Contributor

@okooo5km okooo5km commented May 4, 2026

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-compressionzipic

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

  • Aligns the extension with the existing Zipic.app brand
  • Shortens the AI mention from @images-compression (17 chars) to @zipic (6 chars)
  • Folder, name, and title are all updated consistently

2. New Features

Commands

  • Compress Images — unchanged
  • Compress Images with Options — full-form compression with explicit options: level, format, resize, save location, suffix, subfolder, default-save-directory toggle
  • Compress Images with Preset — reads compression presets directly from the Zipic app's UserDefaults (studio.5km.zipic, key PresetData) 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 presets
  • compress-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

  • Upgrade @raycast/api 1.93 → 1.104, @raycast/utils 1.x → 2.x
  • Upgrade TypeScript 5.4 → 5.9, Prettier 3.3 → 3.8, types/node 20 → 22, types/react 18 → 19
  • Replace post-success showToast + popToRoot with showHUD so confirmations show as an independent on-screen capsule that survives window close
  • Switch console.error to showFailureToast from @raycast/utils
  • Add "platforms": ["macOS"]
  • Strict AI instructions to prevent execution-on-meta-question (e.g. asking "what can you do?" no longer triggers compression)

Checklist

  • npm run lint passes
  • npm run build passes
  • npx ray evals passes (12/12 across 3 consecutive runs)
  • All commands and AI tools tested locally with npm run dev
  • Screenshots regenerated via Window Capture (4 × 2000×1250 PNG)
  • CHANGELOG updated with {PR_MERGE_DATE} placeholder

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

  1. The current AI @-mention syntax forces the slug into user-facing UX. @images-compression is awkward and discouraging.
  2. The old slug is a generic phrase the Store guidelines specifically advise against — the title "Zipic" matches the macOS app users already have installed.

Happy to discuss if a redirect or any migration concern needs handling on the Store side.

- 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"]`.
Copilot AI review requested due to automatic review settings May 4, 2026 16:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 zipic and 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 defaults and 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

Comment on lines +358 to +363
const url = `zipic://compress?${urlParams}`;

try {
// Execute compression command
exec(`open "${url}"`);
} catch (error) {
Comment on lines +154 to +180
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);
Comment on lines +403 to +406
// If adding subfolder
if (addSubfolder) {
outputDir = path.join(outputDir, "compressed");
}
Comment on lines +30 to +47
"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
]
}
]
Comment on lines +29 to +31
### Manage Presets

Create, edit, and manage your compression presets.
Comment on lines +60 to +66
### 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
Comment on lines 3 to 10
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;
}
}
Comment on lines +15 to +30
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,
})),
};
Comment on lines +256 to +265
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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR renames the images-compression extension to zipic, adds two new commands (Compress Images with Options, Compress Images with Preset), and introduces an AI extension with three tools (optimize-images, list-presets, compress-with-preset). Several issues raised in earlier review rounds have been resolved: the open npm package has been removed, MAX_REDIRECTS guards recursive downloads, the write stream is now deferred until a 200 response is confirmed, buildCompressURL correctly skips width/height when zero, and compress-with-preset wraps open() in a try/catch.

Confidence Score: 5/5

The 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

Filename Overview
extensions/zipic/package.json Well-structured manifest: schema ref, categories, platforms, AI tools, and evals all present; unused open npm package removed; dependency list is clean
extensions/zipic/src/compressImages.tsx Simple no-view command; uses showHUD for confirmation and showFailureToast for errors — correct pattern throughout
extensions/zipic/src/compressImagesWithOptions.tsx Form command with full options UI; addSubfolder checkbox has no corresponding subfolder-name input, so the URL scheme receives the flag without a name value — inconsistent with the AI tool which defaults to "zipic"
extensions/zipic/src/compressImagesWithPreset.tsx Preset picker list command; readZipicPresets is wrapped in try/catch, open() in try/catch — error handling is correct here
extensions/zipic/src/tools/optimize-images.ts Comprehensive AI tool with redirect depth guard (MAX_REDIRECTS=5), write stream now deferred until 200, and open() wrapped in try/catch; expectedOutputPaths subfolder calculation uses hardcoded "compressed" instead of the subfolder param value
extensions/zipic/src/tools/list-presets.ts Thin wrapper around readZipicPresets; error handling discussed in previous review rounds
extensions/zipic/src/tools/compress-with-preset.ts Preset resolution logic is solid; open() is now wrapped in try/catch; error handling for readZipicPresets discussed in previous review rounds
extensions/zipic/src/utils/zipicPresets.ts buildCompressURL now correctly guards width/height against zero values; readZipicPresets throw behaviour discussed in previous rounds
extensions/zipic/src/utils/checkInstall.ts Clean installation check; shows Toast with deep-link action when Zipic is absent
Prompt To Fix All With AI
Fix 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

Comment thread extensions/zipic/src/tools/optimize-images.ts
Comment thread extensions/zipic/src/tools/optimize-images.ts
Comment thread extensions/zipic/src/compressImagesWithOptions.tsx Outdated
Comment thread extensions/zipic/src/compressImagesWithOptions.tsx Outdated
Comment thread extensions/zipic/package.json Outdated
- 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).
@okooo5km
Copy link
Copy Markdown
Contributor Author

okooo5km commented May 5, 2026

👋 Heads up on the failing Check workflow: it's trying to cd into extensions/images-compression/, which no longer exists because this PR renames the folder to extensions/zipic/.

cd: /Users/runner/work/extensions/extensions/extensions/images-compression/: No such file or directory
sparse_checkout_directories: extensions/images-compression/
                            extensions/zipic/

The other CI jobs (NPM Check on both extensions/images-compression/ and extensions/zipic/, Greptile, Socket Security, etc.) all pass — only this one fails because it can't enter the deleted directory.

Could you advise on the right way to handle this? A couple of options I can think of:

  • Maintainer override / re-trigger after the rename is merged
  • A staged approach (keep an extensions/images-compression/ placeholder for one release cycle then delete in a follow-up PR)

Happy to take whichever path you prefer. The local npm run lint, npm run build, and npx ray evals (12/12 across multiple runs) all pass cleanly inside extensions/zipic/.

Comment thread extensions/zipic/src/tools/optimize-images.ts
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.
Comment on lines +44 to +60

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +15 to +17
const { presets, selectedPresetId } = await readZipicPresets();

return {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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

Comment on lines +32 to +33
const { presets, selectedPresetId } = await readZipicPresets();
if (presets.length === 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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)
Comment thread extensions/zipic/src/utils/zipicPresets.ts Outdated
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.
Comment thread extensions/zipic/src/tools/compress-with-preset.ts
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants