Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,20 @@ jobs:
tag_name: ${{ github.ref_name }}
name: StackFerry ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
make_latest: "false"
body_path: ${{ steps.release-notes.outputs.path }}
files: release-assets/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Attach trampoline latest.json to GitHub Latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
test -f release-assets/latest.json
LATEST_TAG=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq .tag_name)
if [ "$LATEST_TAG" = "$GITHUB_REF_NAME" ]; then
echo "Trampoline release became GitHub Latest; Electron updater would lose latest.yml" >&2
exit 1
fi
gh release upload "$LATEST_TAG" release-assets/latest.json --clobber
50 changes: 48 additions & 2 deletions announcements/announcements.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,59 @@
{
"schemaVersion": 1,
"generatedAt": "2026-09-04T09:34:00Z",
"generatedAt": "2026-09-10T06:00:00Z",
"announcements": [
{
"id": "2026-09-electron-migrate",
"category": "maintenance",
"severity": "critical",
"publishedAt": "2026-09-10T06:00:00Z",
"expiresAt": null,
"minAppVersion": null,
"maxAppVersion": "0.1.28",
"platforms": ["windows", "macos", "linux"],
"channels": ["stable"],
"relatedVersion": "0.1.28",
"locales": {
"zh": {
"title": "请安装新版 StackFerry",
"summary": "桌面端已经换新。请从 GitHub Latest 下载 Electron 安装包。",
"body": "当前 GitHub Latest 已是新桌面端(Electron 1.0.3)。应用内更新无法把这一版 Tauri 客户端直接换成新软件,请打开 GitHub Latest 下载对应系统的安装包。\n\n本应用数据目录里的供应商列表不会自动迁移。~/.codex、~/.claude、~/.grok 里的 CLI 配置可以继续用。\n\nWindows 和 Linux 可以先和新版并存,再卸载这一份。macOS 安装包未签名,第一次打开可能被 Gatekeeper 拦截。"
},
"en": {
"title": "Install the new StackFerry",
"summary": "This desktop app has been rebuilt. Download the Electron installer from GitHub Latest.",
"body": "GitHub Latest now hosts the new desktop app (Electron 1.0.3). In-app update cannot replace this Tauri build with that installer, so open GitHub Latest and download the package for your OS.\n\nProvider settings live in this app’s data folder and will not copy themselves over. CLI config in ~/.codex, ~/.claude, and ~/.grok is unchanged.\n\nWindows and Linux can install the new build alongside this one, then uninstall this copy. macOS builds are unsigned; Gatekeeper may block the first launch."
},
"zh-TW": {
"title": "請安裝新版 StackFerry",
"summary": "桌面端已經換新。請從 GitHub Latest 下載 Electron 安裝包。",
"body": "目前 GitHub Latest 已是新桌面端(Electron 1.0.3)。應用程式內更新無法把這一版 Tauri 用戶端直接換成新軟體,請打開 GitHub Latest 下載對應系統的安裝包。\n\n本應用資料目錄裡的供應商清單不會自動遷移。~/.codex、~/.claude、~/.grok 裡的 CLI 設定可以繼續使用。\n\nWindows 與 Linux 可以先與新版並存,再解除安裝這一份。macOS 安裝包未簽名,第一次開啟可能被 Gatekeeper 攔截。"
},
"ja": {
"title": "新しい StackFerry をインストールしてください",
"summary": "デスクトップ版は作り直されました。GitHub Latest から Electron インストーラーをダウンロードしてください。",
"body": "GitHub Latest は新しいデスクトップ版(Electron 1.0.3)です。アプリ内更新では、この Tauri 版を新しいインストーラーに置き換えられません。GitHub Latest を開いて、お使いの OS 用パッケージをダウンロードしてください。\n\nこのアプリのデータフォルダーにあるプロバイダー一覧は自動では移行されません。~/.codex、~/.claude、~/.grok の CLI 設定はそのまま使えます。\n\nWindows と Linux では新版と併存してから、このコピーをアンインストールできます。macOS ビルドは未署名のため、初回起動が Gatekeeper にブロックされる場合があります。"
}
},
"actions": [
{
"type": "external",
"url": "https://github.com/Ninthless/StackFerry/releases/latest",
"label": {
"zh": "下载新版 StackFerry",
"en": "Download new StackFerry",
"zh-TW": "下載新版 StackFerry",
"ja": "新しい StackFerry をダウンロード"
}
}
]
},
{
"id": "2026-09-stackferry-rebuild",
"category": "maintenance",
"severity": "important",
"publishedAt": "2026-09-04T09:34:00Z",
"expiresAt": null,
"expiresAt": "2026-09-10T00:00:00Z",
"minAppVersion": null,
"maxAppVersion": null,
"platforms": ["windows", "macos", "linux"],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stackferry",
"version": "0.1.27",
"version": "0.1.28",
"description": "Provider and configuration manager for AI coding tools",
"type": "module",
"packageManager": "pnpm@10.12.3",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "stackferry"
version = "0.1.27"
version = "0.1.28"
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Ninthless", "CC Switch contributors"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "StackFerry",
"version": "0.1.27",
"version": "0.1.28",
"identifier": "com.stackferry.desktop",
"build": {
"frontendDist": "../dist",
Expand Down
2 changes: 2 additions & 0 deletions src/app/overlays/AppOverlays.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import UsageScriptModal from "@/features/usage/UsageScriptModal";
import { ConfirmDialog } from "@/shared/ui/ConfirmDialog";
import { CriticalAnnouncementDialog } from "@/features/announcements/CriticalAnnouncementDialog";
import { DeepLinkImportDialog } from "./DeepLinkImportDialog";
import { ElectronMigrateDialog } from "./ElectronMigrateDialog";
import { FirstRunNoticeDialog } from "./FirstRunNoticeDialog";
import type { AppView } from "@/app/shell/types";

Expand Down Expand Up @@ -180,6 +181,7 @@ export function AppOverlays({
onCancel={() => onLaunchDashboardOpenChange(false)}
/>
<DeepLinkImportDialog />
<ElectronMigrateDialog />
<FirstRunNoticeDialog />
</>
);
Expand Down
68 changes: 68 additions & 0 deletions src/app/overlays/ElectronMigrateDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useState } from "react";
import { ArrowUpCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
import { settingsApi } from "@/platform/tauri/api";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import {
ELECTRON_DOWNLOAD_URL,
ELECTRON_MIGRATE_DISMISSED_KEY,
} from "@/lib/electronMigrate";

export function ElectronMigrateDialog() {
const { t } = useTranslation();
const [open, setOpen] = useState(
() => localStorage.getItem(ELECTRON_MIGRATE_DISMISSED_KEY) !== "1",
);
const [opening, setOpening] = useState(false);

async function handleDownload(): Promise<void> {
setOpening(true);
try {
await settingsApi.openExternal(ELECTRON_DOWNLOAD_URL);
} finally {
setOpening(false);
}
}

function handleLater(): void {
localStorage.setItem(ELECTRON_MIGRATE_DISMISSED_KEY, "1");
setOpen(false);
}

return (
<Dialog open={open}>
<DialogContent
className="max-w-md"
zIndex="top"
onEscapeKeyDown={(event) => event.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ArrowUpCircle className="h-5 w-5" />
{t("electronMigrate.title")}
</DialogTitle>
<DialogDescription>{t("electronMigrate.summary")}</DialogDescription>
</DialogHeader>
<div className="whitespace-pre-line px-6 py-5 text-sm leading-6 text-foreground/85">
{t("electronMigrate.body")}
</div>
<DialogFooter className="justify-between sm:justify-between">
<Button type="button" variant="outline" onClick={handleLater}>
{t("electronMigrate.later")}
</Button>
<Button type="button" onClick={() => void handleDownload()} disabled={opening}>
{opening ? t("electronMigrate.opening") : t("electronMigrate.download")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
8 changes: 8 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
"bodyOfficial": "An “Official” preset is also included in the list — click it any time you want to switch back to the official default. StackFerry automatically backs up your current config to “default” before switching, so you can move back and forth freely. That's how StackFerry works 😊",
"confirm": "Got it"
},
"electronMigrate": {
"title": "Install the new StackFerry",
"summary": "This desktop app has been rebuilt. Download the current GitHub Latest installer.",
"body": "The version you are running is the last Tauri build.\n\nProvider settings live in this app’s data folder and will not copy themselves over. CLI config in ~/.codex, ~/.claude, and ~/.grok is unchanged.\n\nWindows and Linux can install the new build alongside this one, then uninstall this copy. macOS builds are unsigned; Gatekeeper may block the first launch.",
"download": "Download new StackFerry",
"opening": "Opening…",
"later": "Later"
},
"apiKeyInput": {
"placeholder": "Enter API Key",
"show": "Show API Key",
Expand Down
8 changes: 8 additions & 0 deletions src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
"bodyOfficial": "リストには「Official(公式)」プリセットも用意されており、公式バージョンに戻したくなったらクリックするだけで切り替えられます。切り替える前に現在の設定は自動で default にバックアップされるので、自由に行き来できます。これが StackFerry の仕組みです 😊",
"confirm": "了解しました"
},
"electronMigrate": {
"title": "新しい StackFerry をインストールしてください",
"summary": "デスクトップ版は作り直されました。GitHub Latest のインストーラーをダウンロードしてください。",
"body": "実行中のビルドは最後の Tauri 版です。\n\nこのアプリのデータフォルダーにあるプロバイダー一覧は自動では移行されません。~/.codex、~/.claude、~/.grok の CLI 設定はそのまま使えます。\n\nWindows と Linux では新版と併存してから、このコピーをアンインストールできます。macOS ビルドは未署名のため、初回起動が Gatekeeper にブロックされる場合があります。",
"download": "新しい StackFerry をダウンロード",
"opening": "開いています…",
"later": "後で"
},
"apiKeyInput": {
"placeholder": "API Key を入力",
"show": "API Key を表示",
Expand Down
8 changes: 8 additions & 0 deletions src/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
"bodyOfficial": "清單中還預設了「官方 (Official)」供應商,隨時點擊即可切換回官方預設設定。切換前 StackFerry 會自動將目前設定備份回 default,可以放心來回切換。這就是 StackFerry 的運作方式。",
"confirm": "我了解了"
},
"electronMigrate": {
"title": "請安裝新版 StackFerry",
"summary": "桌面端已經換新。請下載 GitHub Latest 上的安裝包。",
"body": "你正在使用的是最後一版 Tauri 用戶端。\n\n本應用資料目錄裡的供應商清單不會自動遷移。~/.codex、~/.claude、~/.grok 裡的 CLI 設定可以繼續使用。\n\nWindows 與 Linux 可以先與新版並存,再解除安裝這一份。macOS 安裝包未簽名,第一次開啟可能被 Gatekeeper 攔截。",
"download": "下載新版 StackFerry",
"opening": "正在開啟…",
"later": "稍後"
},
"apiKeyInput": {
"placeholder": "請輸入 API Key",
"show": "顯示 API Key",
Expand Down
8 changes: 8 additions & 0 deletions src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
"bodyOfficial": "列表里还预置了 “官方(Official)” 供应商,随时点一下即可切回官方默认配置。切换前 StackFerry 会自动把当前配置备份回 default,可以放心来回切。StackFerry 就是这样工作的 😊",
"confirm": "我知道了"
},
"electronMigrate": {
"title": "请安装新版 StackFerry",
"summary": "桌面端已经换新。请下载 GitHub Latest 上的安装包。",
"body": "你正在使用的是最后一版 Tauri 客户端。\n\n本应用数据目录里的供应商列表不会自动迁移。~/.codex、~/.claude、~/.grok 里的 CLI 配置可以继续用。\n\nWindows 和 Linux 可以先和新版并存,再卸载这一份。macOS 安装包未签名,第一次打开可能被 Gatekeeper 拦截。",
"download": "下载新版 StackFerry",
"opening": "正在打开…",
"later": "稍后"
},
"apiKeyInput": {
"placeholder": "请输入API Key",
"show": "显示API Key",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/electronMigrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const ELECTRON_DOWNLOAD_URL =
"https://github.com/Ninthless/StackFerry/releases/latest";

export const ELECTRON_MIGRATE_DISMISSED_KEY = "stackferry:electronMigrate:dismissed";
42 changes: 42 additions & 0 deletions tests/components/ElectronMigrateDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ElectronMigrateDialog } from "@/app/overlays/ElectronMigrateDialog";
import { ELECTRON_DOWNLOAD_URL, ELECTRON_MIGRATE_DISMISSED_KEY } from "@/lib/electronMigrate";

const openExternal = vi.fn().mockResolvedValue(undefined);

vi.mock("@/platform/tauri/api", () => ({
settingsApi: {
openExternal: (...args: unknown[]) => openExternal(...args),
},
}));

describe("ElectronMigrateDialog", () => {
beforeEach(() => {
openExternal.mockClear();
localStorage.clear();
});

it("opens the Electron GitHub Latest download", async () => {
render(<ElectronMigrateDialog />);
fireEvent.click(screen.getByRole("button", { name: "electronMigrate.download" }));
await waitFor(() => {
expect(openExternal).toHaveBeenCalledWith(ELECTRON_DOWNLOAD_URL);
});
expect(ELECTRON_DOWNLOAD_URL).toBe(
"https://github.com/Ninthless/StackFerry/releases/latest",
);
});

it("stays closed after the user postpones migration", () => {
localStorage.setItem(ELECTRON_MIGRATE_DISMISSED_KEY, "1");
render(<ElectronMigrateDialog />);
expect(screen.queryByText("electronMigrate.title")).toBeNull();
});

it("persists postpone so the next launch stays quiet", () => {
render(<ElectronMigrateDialog />);
fireEvent.click(screen.getByRole("button", { name: "electronMigrate.later" }));
expect(localStorage.getItem(ELECTRON_MIGRATE_DISMISSED_KEY)).toBe("1");
});
});
Loading