In the "no active sessions" empty state, clicking the gear icon was
calling viewModel.toggleMenu() which rendered NotchMenuView — but
that fallback menu only contains a single SystemSettingsRow, so
users saw a pointless intermediate screen ("返回 | 退出" header +
one "设置 >" row) before reaching the real settings.
Wire the gear straight to SystemSettingsWindow.shared.show() so the
settings panel opens in one click. NotchMenuView can still be
reached via the hamburger icon in the opened-with-sessions header.
The loadedBundleIdentifiers set was a dedup guard at loadPlugin's top
(prevents the in-app builtin copy from clashing with a newer user-
installed one in ~/.config/codeisland/plugins/). But neither
unload(id:) nor unloadAll() cleared it, so:
install → loadedBundleIdentifiers = {music-player-id}
uninstall → bundle file deleted, loadedPlugins removed
→ loadedBundleIdentifiers still has {music-player-id}
reinstall → copyItem to dest OK
→ loadPlugin checks contains(bundleId) → true → silent return
→ UI shows "installed", loadedPlugins empty, plugin invisible
Fix: remove bundle identifier from the set on unload and unloadAll.
Subsequent loadPlugin for the same bundle id now passes the dedup
check and actually registers the plugin.
The miomio.chat plugin marketplace re-wraps uploaded plugin bundles
into an outer zip so the user-facing download filename matches the
plugin's marketing name (e.g. "Music Player · 音乐播放器-2.3.0.zip").
That produces a nested structure:
outer.zip
└── music-player-v2.3.0.zip ← our original upload (file, not dir)
└── music-player.bundle/
└── Contents/...
installFromURL extracted once with ditto, findBundle recursively
scanned for .bundle, found nothing (because the extracted content is
a .zip file, not a .bundle directory) and aborted with
"No .bundle found in archive".
Fix: after the first extraction, if no .bundle is present AND the
tree contains exactly one .zip, extract that inner zip and search
again. Single-step nested unwrap; does not recurse indefinitely so
we can't get tricked into a zip bomb.
notch customization (live edit)
- hardwareNotchWidth: use auxiliaryTopLeftArea/TopRightArea instead of
safeAreaInsets.left/right. macOS only exposes notch height via
safeAreaInsets.top; left/right are always zero, so the live-edit
dashed border was rendering at full screen width on MacBook.
- NotchView.closedNotchSize: stop pinning height to deviceNotchRect on
hardware-notched machines. Now always reads the user's geo.notchHeight,
so height arrows in the editor actually resize the island.
- NotchLiveEditOverlay: removed .disabled(hasHardwareNotch) guard on
height arrows. Software island height is independent of the physical
camera cutout.
- NotchLiveEditPanel: height 160 -> 220. Old size clipped the
Save/Cancel row when the user set any non-trivial notch height.
plugin panel
- NativePluginManager: height clamp floor 180 -> 120. Allows plugins
like the music card to claim a compact panel.
- PluginContentView: replaced the fixed back-button header row with a
floating chevron chip (ZStack overlay). Plugins now get the full
panel area and can paint theme color top-to-bottom.
- PairPhoneView: absorbed the ~20pt clearance that used to come from
host chrome by raising its own .padding(.top) from 16 to 44.
- BuiltInPlugins: pair-phone panel height 480 -> 580. Fits header,
QR, short code, server row, and a couple of linked devices without
a scrollbar.
Settings window (SystemSettingsView):
- Graphite dark palette (#201f27 sidebar / #1c1c1e detail) replacing the
lime L-shape, per Claude Design reference bundle. Lime survives only
as an accent on toggles, icons, and active pills.
- macOS-style titlebar with real traffic lights (red close / yellow
hide / green decorative) + centered "系统设置".
- Tabs get large H1 + English subtitle ("通用 General preferences").
- New primitives: SectionLabel, SettingsListCard, SettingRow
(icon tile + label + sublabel + control), InfoRow (pos/neg/hint
colored dots), IOSToggle (pill slider).
- General tab rewritten: stacked rows with dividers, 3 toggles with
sublabels, proxy card with ✓/✕/i info rows, language Menu picker,
accessibility status row.
- Bottom sidebar "返回" → "退出" calling NSApplication.terminate.
- Window resized 720×560 → 960×720 (1.33:1) for reference-mock breathing
room. Still fixed-size (borderless).
- TextField placeholder: ZStack overlay with solid light gray, since
SwiftUI TextField.prompt ignores foregroundColor on macOS. Applied to
both the Anthropic proxy field and the Install-from-URL field.
Notch themes (NotchTheme / NotchCustomization):
- Reset to 7 themes: Classic + Forest / Neon Tokyo / Sunset /
Retro Arcade / High Contrast / Sakura (ported from Claude Design
themes.jsx palettes). Dropped: paper, neonLime, cyber, mint, rosegold,
ocean, aurora, mocha, lavender, cherry.
- Graceful decode: try? c.decode(NotchThemeID) so legacy raw values
fall back to .classic instead of throwing.
- NotchPalette gains `accent` field. NotchView.statusDotColor and
badgeColor use accent for .idle, so at-rest notch reflects the theme
instead of hardcoded 30%-white (invisible on light-bg themes).
- Theme picker replaced Menu dropdown with 2-column grid of preview
cards, each rendering a mini pill in that theme's own colors. Selected
card borders/glows in its own accent.
Buddy style (NotchCustomization.BuddyStyle):
- New `buddyStyle: BuddyStyle` field with two cases: .pixelCat, .emoji.
Evaluated a .neon option via NeonPixelCatView but it degrades to a
green blob at 16×16; pulled from the picker pending a small-size
renderer.
- Migration: missing buddyStyle decodes by reading the legacy
usePixelCat AppStorage bool. Picker writes back to usePixelCat so
unmigrated call sites (ClaudeInstancesView) stay in sync.
- Old "Pixel Cat Mode" toggle removed from Appearance tab — the new
segmented picker in the Notch section supersedes it.
Plugin panel size hint for built-ins:
- LoadedPlugin.preferredPanelSize checks an @objc runtime method first,
then falls back to Info.plist (gated on bundle !== Bundle.main so
built-ins don't accidentally read the host's plist).
- PairPhonePlugin declares @objc preferredPanelSize() = 340×480, then
bumped to 440×480 to match the music plugin's width.
Tests:
- NotchThemeTests / NotchCustomizationTests / NotchCustomizationStoreTests
updated to new theme line-up plus a regression guard for the legacy
theme ID → .classic fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes prep for v2.1.8:
1. Plugin size hint API
Plugins can now declare preferred expanded panel dimensions via two
optional Info.plist keys:
MioPluginPreferredWidth (Number, 280..1200)
MioPluginPreferredHeight (Number, 180..900)
When both are present, the host caps the expanded panel to the
requested size (clamped to 95% of screen dimensions so a plugin
can't escape the display). Missing or out-of-range values fall
back to the default ~620×780.
- NativePluginManager.LoadedPlugin grows a `preferredPanelSize: CGSize?`
computed property reading Info.plist at lookup time.
- NotchViewModel.openedSize for .plugin(let pluginId) consults
NativePluginManager.shared.plugin(id:)?.preferredPanelSize before
falling back to the old default.
Motivation: mio-plugin-music ships as a compact card. At the default
620×780 the card floats in ~500pt of dead vertical space.
2. Info.plist: NSAppleEventsUsageDescription
The v2.1.7 binary shipped with this key (release.sh picked it up from
disk at build time), but the v2.1.7 source commit only included the
pbxproj bump. Backfilling the source so git matches what's running
in /Applications.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace docs/wechat-qr.jpg with current group invite (was expiring)
- DEVELOPER-SETUP pointed at docs/wechat-group-qr.jpg which doesn't
exist; fix to docs/wechat-qr.jpg and note the 7-day WeChat TTL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Companion to the VITE_LATEST_VERSION injection in landing/deploy-landing.yml.
That workflow reads the latest release tag at build time, but it only
runs on pushes to the landing-page branch. This workflow listens for
release events on main (published / edited / released) and dispatches
deploy-landing so the Mac DMG download link gets refreshed without a
manual commit to landing-page.
Flow:
1. Admin runs release.sh and `gh release create vX.Y.Z ...`
2. GitHub fires `release: published`
3. This workflow dispatches deploy-landing.yml on landing-page
4. deploy-landing fetches the new tag and rebuilds the site
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clean, no-secrets walkthrough for getting a dev environment up. Covers
build, directory layout, commit conventions, what not to commit (key
files, .sparkle-keys, build artifacts), and common gotchas.
Complements docs/RELEASE-GUIDE.md which is admin-only and covers the
actual release flow + key management.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Marketing version stays at 2.1.6 but build increments to 23 so Sparkle
picks up the re-signed DMG as a newer release. The v2.1.6 tag still
points at build 22 (original release) — intentional, this re-release
is a security patch, not a new feature drop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old key: 2099yGC8...ih2Q= (private key was accidentally committed to
the repo in a547d434 and must be treated as compromised).
New key: XzmsCyH9tALFSPHRD/P5D5/r7MNV3loKYyOxZMEXblg=
New private key is stored locally at .sparkle-keys/eddsa_private_key
(gitignored) and is distributed out of band to release admins.
The v2.1.6 (build 23) bridge release is signed with the OLD private key
so existing v2.1.5 / v2.1.6 (build 22) users can auto-update one last
time. After that, their Info.plist will contain the new public key and
all future releases (v2.1.7+) are signed with the new private key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous commit (a547d434) committed the Sparkle signing private key
inline in the doc. HEAD no longer exposes it, but the blob is still
reachable via git history (a547d434:docs/RELEASE-GUIDE.md).
Next step is either rewriting history (requires temporarily relaxing
main branch protection) or rotating to a fresh keypair. See incident
discussion outside the repo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Active sessions with a tool call preview + inline approval buttons can
render ~130pt tall, but the panel height used perSession=65 + baseHeight=100
which left only ~115pt of ScrollView area after header + bottom padding.
The bottom row ended up under the buddy/stats overlay even though the
ScrollView could technically scroll to it.
Bumps to baseHeight=120 / perSession=100 — gives 1 session 220pt, 2 sessions
320pt, etc. Still capped by expandedMax so large session counts still fit
inside 65% of screen height.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
release.sh previously printed "SKIP Sparkle signing" when the private
key was absent on the release machine, but then still generated and
pushed an appcast with sparkle:edSignature="". This is exactly how
v2.1.6 shipped unsigned — every user saw "此更新未正确签名".
Now: if ED_SIG is empty after the sign_update step, exit 1 with a
diagnostic pointing at docs/RELEASE-GUIDE.md §3 (canonical key).
Also copies the release guide into the repo at docs/RELEASE-GUIDE.md
so new admins don't need the chat-history PDF. Includes the v2.1.6
incident + emergency-resign recipe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
isVisible was initialized to false, causing opacity=0 for 1 render frame
before onAppear fired. On notched Macs this meant the standby content
would flash invisible on launch. Start it true so the notch is visible
from frame 1 — the app always shows standby content regardless of sessions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs prevented the standby indicator from appearing when no sessions
are active on physical-notch MacBooks:
1. onAppear only set isVisible=true for non-notched devices, so notched
Macs started invisible and never showed standby at launch.
2. handleStatusChange(.closed) set isVisible=false whenever the notch
closed with no active sessions, hiding the standby content.
Fix: always set isVisible=true on appear, and never hide on close since
standby content fills the notch when there are no active sessions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display pixel cat icon + "待机中/Standby" text in collapsed notch
when there are no Claude Code sessions, so users know the app is running.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Includes Sparkle signing workflow, key setup, troubleshooting,
and step-by-step instructions for new release managers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add zh-Hans.lproj and en.lproj so Sparkle shows Chinese update dialogs
when system language is Chinese.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sparkle compares sparkle:version against CFBundleVersion (build number),
not CFBundleShortVersionString. Auto-increment build number on each
release and use it in appcast for correct version comparison.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
miomio.chat is the plugin store, not GitHub Pages.
The appcast is hosted at miomioos.github.io/MioIsland/appcast.xml.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Integrate Sparkle 2.6+ via SPM for EdDSA-signed auto-updates
- Add UpdaterManager wrapper with observable canCheckForUpdates state
- Add "Check for Updates" button in Settings → About tab
- Configure SUFeedURL pointing to miomio.chat/appcast.xml
- Generate and store EdDSA public key in Info.plist
- Add zh-Hans to knownRegions for Sparkle UI localization
- Rewrite release.sh: auto-detect DerivedData, create DMG,
Sparkle EdDSA signing, appcast.xml generation, and auto-deploy
appcast to landing-page branch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without create-dmg installed, the DMG only contained the app with no
drag-to-install target. Add an Applications symlink so both paths
produce a usable installer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix brew install command: remove --cask flag, use correct tap path
- Add MioIsland WeChat user group QR code to contact section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug fixes:
- fix(#60): probeAutomationPermission passed requestorAddr instead of
targetAddr to AEDeterminePermissionToAutomateTarget, causing the
AE permission check to query the wrong app and hang indefinitely,
freezing the entire settings panel
- fix(#59): discoverClaudeSessionsFromConfig used runShellWithTimeout
to spawn /bin/ps for pid liveness checks, which fails under certain
code-signing configurations. Replaced with kill(pid, 0) signal check
— faster, no subprocess needed, works in all environments
UI improvements:
- Add "Quit Mio Island" button at bottom of Settings → About tab
- Anthropic API Proxy description: improve readability (medium weight,
gray-white color, wider line spacing), update CodeIsland → MioIsland
- TextField placeholders: change from default black to light gray
(white 30% opacity) for both proxy URL and plugin install URL fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>