Skip to content

Roadmap zh HK

ChiFungHillmanChan edited this page Oct 13, 2026 · 24 revisions

Roadmap(路線圖)

English version: Roadmap

V0.1 出咗一個最小、可 ship 嘅 slice(7 個 hardcoded layout、hotkey、AX permission、DMG)。V0.2 + V0.3 + V0.4 bundle 埋一齊做第一個公開 release(v0.4.0,2026 年 4 月):自訂、smooth animation、drag-to-swap、Workspaces、thumbnail、縱向 seed、多語 UI。V0.4.1、V0.4.2、V0.4.3 係後續 patch。V0.5.0 出咗 Apple notarization,V0.5.1 加咗持續 update detection,V0.5.2 加咗首次啟動歡迎畫面,V0.5.3 調順咗動畫同埋加返 Intel 支援(universal binary),V0.5.4 修咗首次啟動嘅 welcome 同 Accessibility 升級時嘅 stuck 救援,V0.5.5 修咗 quit + relaunch 之後 24 小時冷卻吞咗新版本通知嘅 bug 同埋 Workspaces tab 加返 delete 掣,V0.5.6 加咗 in-app update installer,V0.5.7 加咗用家自訂排列 + 拖邊同步 + Save 掣 dirty-state,V0.6.0 加咗 privacy-first 診斷紀錄 + 持續性 Workspace(pinned apps + 指定 Desktop + enforcement modes),V0.6.1 加咗閒置模式(Free Mode):menu bar 撳一掣,Setpiece 所有自動行為暫停,唔使 quit。V0.7.0 開咗自動化界面(scene:// URL scheme + AppIntents,可以用 Shortcuts / Siri)、加咗 Workspace 每個螢幕獨立 layout,仲修咗拖拽對調同 Free Mode 撳唔返轉頭嘅問題。V0.7.1 修咗上下唔對稱嘅 layout 上下倒轉 apply,同埋 seam-resize 喺 canvas 自訂 layout 度亂咁推窗口嘅問題。V0.7.2 幫 Settings 視窗換上 macOS 26 嘅 Liquid Glass 設計 — Tahoe 上面透明 chrome、浮起玻璃 sidebar、成個視窗半透明。V0.7.3 令再撳同一個 layout 唔會再搞亂已經入位嘅 window,同埋 menu bar 個 menu 撳完唔會即刻閂。V0.7.4 令 layout 嘅幾何唔再受 Dock 喺邊部 mon 影響,多 mon 機再撳同一個 layout 真係完全唔會郁,menu panel 會為生效嗰個 layout 打剔,仲修咗更新檢查會拖住用家一版一版咁爬嘅問題。V0.7.5 修返 macOS 26 上面 Settings 嘅工具列 — V0.7.2 嗰層透明背景搞到 Layouts 同 Workspaces 所有新增/複製/刪除嘅掣通通唔見咗。 V0.7.6 令對話框、sheet 同浮動面板唔會再俾人當成可以排嘅窗,將 V0.7.4 嗰個「每個螢幕都預留 Dock 空間」變成可以熄,令啲窗貼得返底邊,仲修咗一個新版設定檔會令舊版開唔到嘅啟動崩潰。

v0.8.2 已出

  • 拖拽對調同接縫調整而家可以同時喺每個螢幕運作。 兩個螢幕嘅 Mac 有三個問題,全部喺真 MacBook + 外置 mon 重現過:拖咗去另一個螢幕嘅窗仍然記住喺舊格,下次對調就被扯返去;放手時滑鼠喺另一個螢幕、但個窗中心仲喺 layout 嗰個螢幕,就會同 macOS 自己搬螢幕打交,個窗一半喺一邊、尺寸亂晒;coordinator 得一份全域 snapshot,所以喺一個螢幕 apply 就會令另一個螢幕嘅拖拽對調失效。
  • 修正:PlacementRegistry,即係呢份路線圖之前計劃嘅 per-screen snapshot map。 每個螢幕一份 snapshot。記錄一個螢幕嗰陣會將佢啲窗由其他 snapshot 剔走,同埋清走拔咗嘅螢幕;controller 會搵返擁有被拖嗰個窗嘅 snapshot。AXMoveObserverGroup.setObserved 會跨螢幕比較前後嘅 observed set,唔再每次 apply 都拆晒重砌。DragSwapController 會將「放落 layout 以外嘅螢幕」當成個窗離開咗 layout(onWindowsLeftLayout),已經離開咗嘅讓位窗永遠唔會被扯返嚟。
  • 陷阱:判斷「滑鼠喺唔喺呢個螢幕」要用 NSMouseInRect,唔好用 CGRect.contains。 喺螢幕最頂一行,NSEvent.mouseLocation.y == frame.maxY,contains 唔計,所以拖經 menu bar 嗰陣會中途放咗個窗。
  • WhatsApp 排得返。 佢個主視窗報 AXSubrole = AXDialog,V0.7.6 嘅 deny-list 會跳過。而家被拒嘅 subrole 只要全螢幕掣 enable,或者最小化同放大都 enable,就會救返(WindowChrome)。AXSheet / AXDrawer 永遠唔救。
  • 「每個螢幕都預留 Dock 空間」預設熄,settings v5 會幫升級嘅用家熄埋。 V0.7.4 想要嘅穩定性用另一個方法保住:只要個窗喺 TilingFrame.stickyCandidates(而家嘅 visibleFrame、預留 Dock、淨係預留 menu bar)任何一個範圍入面對得上自己格,LayoutEngine.plan(alternateFrames:) 就當佢已經入咗位。
  • CI 修好: Xcode project 用 ../../macbook-resizer 搵 SceneCore,repo 改名之後每次 push 都壞;而家改咗做 ..。
  • Tests 438 -> 467。Dock 真係搬螢幕之後再 apply 淨係有 unit test,因為模擬滑鼠事件推唔郁 Dock。喺 macOS 27 驗證過。

v0.8.1 已出

  • macOS 27 上面開設定唔會再凍結兼閃退。 喺 macOS 27.0.1 用 v0.8.0,個窗一出現就凍結,跟住個 process 以 NSGenericException 死咗:"The window has been marked as needing another Update Constraints in Window pass, but it has already had more Update Constraints in Window passes than there are views in the window." 次次開都係咁,因為設定一開就係 Workspaces 嗰版。
  • 原因:一個 NavigationSplitView 揼咗喺另一個入面。 WorkspacesTab 係一個 NavigationSplitView,擺咗喺 SettingsRoot 嗰個嘅 detail column 入面。個窗第一次排版嗰陣,入面嗰個 split 嘅 SwiftUI column 不停報一個新嘅最細尺寸(SplitViewChildController.hostingView(_:didUpdateMinSize:)),每報一次都喺排版途中令 root hosting view 嘅 constraint 失效,於是個 pass 永遠停唔落嚟。喺真 build 入面逐樣排除過:一開係 Layouts(喺同一個 detail column 入面嘅 HSplitView)或者 About 就開得正常,熄咗 macOS 26 嘅 title bar 樣式就照樣死。開咗個窗之後先轉去 Workspaces、拉大拉細、閂咗再開,全部都冇死過。
  • 修正:WorkspacesTab 改用 HSplitView,同 LayoutsTab 一樣。 Toolbar 搬咗去個 split view 度,照樣橋接到去視窗 toolbar,因為 hosting controller 仍然係個窗嘅 root(V0.7.5 嗰個限制)。喺預設視窗大細,editor 嗰格改前改後都係 304 pt 闊。
  • 以後改設定要守嘅規矩:唔好喺 SettingsRoot 嘅 detail column 入面再揼一個 NavigationSplitView。 用 HSplitView。
  • Tests:438,冇變。淨係喺 macOS 27.0.1 驗證過。

v0.8.0 已出

  • Scene 改名做 Setpiece,而任何識別現有安裝嘅嘢都冇變。 Bundle identifier 仍然係 com.hillman.SceneApp,因為 TCC 係綁住 identifier 同簽名嚟記住 Accessibility 授權 — 換一個就等於一次過靜靜雞收回所有人嘅權限,而且 macOS 會繼續顯示個掣係 ON 但 AXIsProcessTrusted 返 false,用家嘅體感係「個 app 壞咗」而唔係「我要重新授權」。Application Support 仍然係 Scene。用家見到嘅係 CFBundleDisplayName。
  • 磁碟上個 bundle 保持 Scene.app,而呢個限制塑造咗成次改名。 直到 v0.7.6 為止出街嘅每個 updater 都會檢查 [ ! -d "$MOUNT/Scene.app" ],搵唔到就中止,所以 DMG 入面換咗檔名就會令成個用戶群嘅 app 內更新爛晒。UpdateInstaller 而家改成攞 DMG 入面嗰個 .app(find "$MOUNT" -maxdepth 1 -name "*.app"),令到呢個 updater 流通咗之後,真正改 bundle 名變成一個版本就做得到嘅事。
  • scene:// 永久保留,setpiece:// 係增加。 用舊 scheme 砌嘅 Shortcut、Raycast script 同書籤,app 由內部完全睇唔到亦搬唔到,所以舊 scheme 永遠唔會拆。URLRouter.acceptedSchemes 收住兩個;五個 test 釘住佢哋,而且攞改名前嘅 router 驗過,喺嗰度有三個會紅。
  • 版本號揀 minor 而唔係 patch。 版本字串係 updater 唯一會推到用家眼前嘅嘢;「0.8.0 — now called Setpiece」有人讀,「0.7.7」會被略過。切版本嗰陣 v0.7.6 得六個下載,所以差唔多成個用戶群係由一個叫 Scene 嘅 build 直接跳去叫 Setpiece 嘅,只會見到一次改名。
  • Xcode 27.0 喺 -O 下 build Release 唔會再出現 SIL optimizer ICE —— 就係嗰個逼我哋喺 Xcode 26.x 加 SWIFT_OPTIMIZATION_LEVEL=-Onone 嘅問題,已針對本專案(universal、deployment target 14.0)驗證。但 v0.8.0 特登保留住個 override:一個改名版本唔應該同時改 codegen,否則出問題就分唔清邊樣搞出嚟。優化版 binary 量到 3.2MB 對 120KB,出之前值得搞清楚。
  • Homebrew token scene → setpiece,用 cask_renames.json 對映舊 token,令 brew upgrade 照樣解析得到。實測:brew info --cask chifunghillmanchan/tap/scene 解析成 chifunghillmanchan/tap/setpiece。
  • 安裝說明補咗 brew trust。 同改名無關:Homebrew 7 對未 trust 嘅第三方 tap 係直接報錯唔會問你,所以之前嗰行單行安裝指令會失敗,而且冇任何解決提示。
  • Tests 433 → 438。 Universal(arm64 + x86_64),macOS 14+,Apple notarized。

已改名:Scene 變成 Setpiece

呢個 app 一直到 v0.7.6 都叫做 Scene。呢個 wiki 由而家開始用新名,但已經裝咗嘅 安裝完全冇變:bundle identifier 仍然係 com.hillman.SceneApp,所以 Accessibility 授權唔會斷;資料仍然喺 ~/Library/Application Support/Scene;磁碟上個 bundle 仍然 係 Scene.app;而 scene:// URL 會同新嘅 setpiece:// 一齊繼續用。今次改名只係 改顯示名稱。

v0.7.6 已出

  • 對話框、sheet、inspector 同浮動面板唔會再被排版。 AXWindowEnumerator 以前收晒所有 layer-0 而 AX frame 對得上 CGWindowList 範圍嘅窗 — 即係一個 app 擁有嘅每一個窗,包括佢啲對話框。開個偏好設定對話框或者「顯示簡介」就會食咗一格、令所有真窗重新洗牌、將最後一個推去 toMinimize:喺 Finder 撳個 ⌘I 就足以搞亂一個三分排版。新嘅 WindowSubrole(SceneCore/AX)會讀 kAXSubroleAttribute,拒絕 AXDialog、AXSystemDialog、AXFloatingWindow、AXSystemFloatingWindow、AXSheet 同 AXDrawer;sheet 同 drawer 係附喺母窗上面,根本冇得獨立擺位。
  • 呢個判斷用排除清單,唔用允許清單。 好多 app 根本冇填 subrole,或者填啲自訂嘢。如果一定要 AXStandardWindow 先排,就會靜靜雞令成個 app 都排唔到,而唔係淨係多咗一個面板 — 呢種失敗用家完全查唔到原因,只會合理咁認為「Setpiece 壞咗」。所以冇填、空白、認唔出嘅一律照排。AXUnknown 同樣特登唔放入清單:佢嘅意思係「個 app 冇講呢個係乜」,係含糊而唔係唔合資格,而且 layer-0 嗰層已經隔走咗通常帶住佢嘅 overlay 窗。呢個唔淨止靠 unit test,仲用真實窗口驗證過:四個到達 layer-0 關卡嘅測試窗入面,AXDialog 同 AXFloatingWindow 嗰兩個而家被隔走,文件窗照排。
  • SettingsStore.dockReserveAllDisplays 令 V0.7.4 嘅取捨變成你嘅選擇。 喺每個螢幕都預留 Dock 厚度,可以令 Dock 游走時 tiling rect 保持穩定,代價係冇 Dock 嗰個螢幕底部有一條死位 — 喺真實雙螢幕上實測係 55pt。設定 → 互動 → Dock 空間 可以熄咗個預留,還原做原始 visibleFrame:啲窗貼實底邊,同時 Dock 游走嘅位移亦跟住返嚟。提示文字直接寫明呢個代價,冇當佢係免費午餐。預設開,升級嘅用家 migrate 之後仍然係開 — 佢哋本身已經係穩定行為,唔應該喺唔知情下改。單螢幕嘅 Mac 開唔開都一樣,因為一個螢幕嘅最大 inset 就係佢自己。
  • 三個用到 tiling rect 嘅地方都讀同一個設定 — LayoutEngine.plan、drag-swap 最近 slot、seam reflow — 因為 drag 如果對住另一個 rect 度,就會 snap 去錯嘅格。
  • 設定 schema v3 → v4,而且之後都向前相容。 AppDelegate.init 會將任何 store 初始化嘅 throw 變成 fatalError,而 decodeWithMigration 撞到任何認唔出嘅 schema version 都會 throw — 所以行過新版再返去舊版嘅人,會得到一個開唔到而且冇任何提示嘅 app。呢個喺今次 release 嘅測試過程中實地重現咗,唔係理論:行咗 v0.7.6 個 build 之後 settings.json 升咗級,跟住 v0.7.5 年代嗰段 code 就喺啟動時死咗,錯誤係 Unsupported settings schema version 4。而家高過 currentVersion 嘅版本會經一個 LenientFile(每個欄位都係 optional)寬鬆解碼:攞呢個 build 識得嘅,其餘用預設。真正唔支援嘅版本仍然會 throw,因為嗰啲係檔案損壞而唔係嚟自未來。個檔案喺載入時特登唔會被改寫,所以返去新版嗰陣設定仍然完好;儲存設定就會用呢個 build 嘅版本改寫,但嗰個係用家自己嘅動作。decodeWithMigration 同時由 4-tuple 改成 struct — 加多一個 Bool 之後變成三個 Bool 排埋一齊,差一個位就係一個 silent bug。
  • 一次過嘅 GitHub star 提示。 StarPromptState(SceneCore)用純值型別收起規則:數真正落地嘅 layout、夠 20 次先問、無論答邊樣都當永久。個計數器喺門檻度封頂,所以長期使用都唔會 overflow;答咗之後就停止郁;解碼寫得保守,人手改過或者未來版本寫嘅內容都唔會令 app crash,亦唔會令已經撳走嘅提示再彈返出嚟。StarPromptTracker(SceneApp)存喺 UserDefaults 而唔係 settings.json,因為佢喺 hotkey 頻率嘅路徑上跳,而 SettingsStore.persist() 係成個檔案 atomic 重寫 — 同 hasShownFirstLaunchWelcomeV1 一樣嘅理由。個 row 擺喺 menu panel 入面 Settings 上面,咁就唔會擠走用家開個 menu 本來想撳嘅 workspace 或者 layout。只有行到 performApplyLayout 尾嘅套用先計數:搵唔到窗嘅會提早 return,唔計。
  • Repository 由 macbook-resizer 改名做 scene-macos,README 亦都改用動態示範,唔再係一張靜態圖加一條 GitHub 唔會內嵌播放嘅 13 MB MP4 連結。GitHub 嘅永久 redirect 已經針對三條已出街版本依賴嘅路徑驗證過:UpdateChecker 會 poll 嘅 releases API(301 → 200)、Homebrew cask 會解析嘅 release asset 下載(200)、同埋網頁 URL(301)。
  • Tests 394 → 433。 十二個 WindowSubroleTests 釘死排除清單嘅合約,包括冇填/空白/認唔出嘅保證;十個 DockReserveTests 釘死兩個螢幕上嘅預留、預設值同單螢幕一致性;四個 SettingsForwardCompatibilityTests 釘死未來檔案嘅處理;十三個 StarPromptStateTests 釘死門檻、永久性同解碼容錯。Universal(arm64 + x86_64),macOS 14+,Apple notarized。

v0.7.5 已出

  • macOS 26 上面 Settings 嘅工具列返晒嚟。 喺 Tahoe 度,工具列啲掣全部唔見咗:Layouts 嗰版嘅 +、+ Custom、刪除、還原 seed 同 Restore Default Presets;Workspaces 嗰版嘅 New、Duplicate 同 Delete。已經有嘅 layout 同 workspace 仲改得、撳 hotkey 又 fire 得、喺 menu bar 都啟動得到 — 但係成個新增/複製/刪除嘅界面都掂唔到,所以 Tahoe 新用家開 Settings 就搵唔到任何方法加嘢。Issue #4。
  • 根本原因:做咗 child 嘅 hosting controller 唔會交出 toolbar。 V0.7.2 為咗做成個視窗嘅透明效果,將一個 NSVisualEffectView 嘅 container view controller 設成視窗嘅 contentViewController,再將 SwiftUI 個 NSHostingController 當 child 咁揼入去、貼實四邊。NSHostingController 淨係喺佢自己就係視窗嘅 content view controller 嗰陣,先會將 SwiftUI 嘅 .toolbar 交去 view.window.toolbar;做咗 child 就乜都唔交 — 冇 warning、冇 compile error、連 runtime log 都冇。結果個視窗係 window.toolbar == nil:唔係得個空 toolbar,而係根本冇 toolbar,所以無論邊個 tab 宣告乜都渲染唔到。
  • 點樣 debug 出嚟。(hosting controller 被包住 vs 做視窗根層)×(Tahoe 嘅 fullSizeContentView + 透明標題列 + 隱藏標題 vs 普通 chrome)四種組合各起一次,layout 完之後查 window.toolbar。淨係嗰兩個被包住嘅情況會得 nil;兩個做根層嘅都有一條三粒掣嘅 toolbar 睇得見。原本最可疑嘅透明標題列(因為佢同呢個 regression 一齊入)其實係無辜嘅。
  • 修法。 NSHostingController 重新做返視窗嘅根 contentViewController,而 .underWindowBackground / .behindWindow 嗰層物料改咗喺 SwiftUI 度做 background 圖層 — 用一個細細嘅 private NSViewRepresentable,再經 WindowBackdrop 呢個 view modifier 套用,macOS 26 以下係 no-op。Liquid Glass 個樣完全冇變 — 浮起玻璃 sidebar、透明標題列、成個視窗半透明 — 而原本分兩條分支嘅視窗建構亦都合併成一條所有 macOS 版本共用嘅路。嗰個 call site 加咗註解寫明呢個陷阱,唔好再踩多次。
  • 影響範圍。 macOS 14 同 15 一直冇事,佢哋行嘅係冇改過嗰條 else 分支。受影響嘅係 v0.7.2、v0.7.3 同 v0.7.4,而且淨係喺 macOS 26。
  • Tests 394/394,冇變。 呢個 bug 喺 SceneApp 入面嘅 AppKit/SwiftUI 視窗底層,而 swift test(設計上淨係跑 framework-neutral 嘅 SceneCore)結構上根本掂唔到 — 所以本來就冇 unit test 捉得到,亦都冇夾硬加個 test 扮捉到。改為直接對住行緊嘅 App 驗證:Layouts 個 toolbar 報返 Add, square.on.square.squareshape.controlhandles, Bin, arrow.counterclockwise, Restore Default Presets,Workspaces 報返 New, Duplicate, Bin。Universal(arm64 + x86_64),macOS 14+,Apple notarized。

v0.7.4 已出

  • Dock 唔再影響 tiling frame。 NSScreen.visibleFrame 淨係喺 Dock 當下嗰部 mon 度留返位,而 Dock 會跟住隻滑鼠喺唔同 mon 之間跳 — 即係話個 tiling rect 睇嘅唔係部 mon,而係你隻滑鼠啱啱去過邊。將 Dock 移去另一部 mon 再撳返同一個 layout,每個 slot 都會照 Dock 嘅厚度郁一次,遠遠超出 V0.7.3 嗰 10 pt 嘅 sticky tolerance,所以全部 window 都會重新縮放再按 z-order 重排 — sticky 嘅保證喺最需要佢嘅 setup 上面靜靜雞失效咗。新嘅 TilingFrame(SceneCore/Display)將 Dock 嘅預留位統一到所有螢幕:左 / 下 / 右 inset 取全部螢幕嘅最大值,上邊嘅 inset 就各自保留,因為 menu bar 唔會跟滑鼠走。由於 Dock 同一時間只會喺一部 mon 度,個最大值本身就係 Dock 嘅厚度,佢移嚟移去都唔會變,所以個 rect 淨係取決於你部部 mon 點擺。畸形嘅排列會 fall back 返 visibleFrame;淨係一部 mon 嘅話,一個螢幕嘅最大值就係佢自己嘅 visibleFrame — 同 V0.7.3 完全一模一樣。已接受嘅代價:冇 Dock 嗰部 mon 都要留返嗰條位;開咗 Dock 自動收埋就完全冇代價。
  • 拖拽同拉邊都用同一個 frame。 DragSwapController 同 SeamResizeController 而家 construct 嗰陣會俾一個指住 TilingFrame 嘅 visibleFrameOverride(重用返本身為咗測試而存在嘅注入位),所以最近 slot 同 reflow 嘅計算都係對住 layout 真正排入去嗰個 frame,而唔係一個爭成 70 pt 嘅 rect。
  • 判斷 window 屬於邊部 mon 改用 screen.frame。 AXWindowEnumerator 嘅範圍檢查唔再用 visibleFrame:中心點落喺 Dock 條位度嘅 window 以前會被完全剔出 plan,而且淨係喺 Dock 啱啱喺嗰部 mon 嗰陣先會發生。「呢個 window 屬於邊部 mon」應該問 frame,visibleFrame 從來都唔係答案。
  • 生效 layout 打剔。 Coordinator.activeLayoutID 係 @Published,而且淨係喺 performApplyLayout 成功嗰條路先會 set,所以搵唔到 window、或者中途冇咗 AX 權限嗰啲情況,都唔會為一個螢幕上根本冇反映到嘅 layout 打剔。MenuBarContentView.layoutRow 會喺 hotkey 鍵位之後畫個剔,個欄位闊度固定而且每一行都有,所以鍵位永遠對齊。同 Free Mode 一樣淨係記憶體狀態 — 重開之後乜都唔會打剔,因為 Setpiece 根本唔知你啲 window 仲係咪排住。
  • 更新檢查自己揀最高版本,唔再信 GitHub 嘅 releases/latest。 嗰個 endpoint 嘅定義係:喺非 prerelease、非 draft 嘅 release 入面,按 created_at 排最新嗰個 — 而 created_at 係個 tag 指住嗰個 commit 嘅日期,唔係發佈日期。所以由舊 commit 切出嚟嘅 hotfix 會排喺佢所取代嗰個 release 下面,落後好多版嘅用家就會收到舊嗰個,裝完重開又收到下一個:一版一版咁爬。UpdateChecker 而家攞 releases?per_page=100,剔走 draft 同 prerelease,再用 indexOfHighestVersion 揀 — 逐個 component 用數字比較,所以 v0.10.0 會正確大過 v0.9.0,用字串排序就會啱啱相反。冇更新嗰陣個 banner 亦都會清走,唔會喺用家自己手動裝完 DMG 之後仲賴死喺度。
  • 測試 371 → 394。 12 個 TilingFrameTests case 釘死 Dock 嘅合約(單螢幕喺兩種 Dock 狀態下都完全一致、Dock 喺 A 定 B 都出同一個 frame、Dock 喺左右兩邊、上邊 inset 逐個螢幕分開、畸形排列 fall back、以及端到端驗證 quad slot rect 完全唔郁)。11 個 HighestVersionSelectionTests case 釘死版本挑選,包括 created_at 次序倒轉嗰個 case 同 v0.9.0 vs v0.10.0 陷阱。Universal(arm64 + x86_64),macOS 14+,Apple notarized。

v0.7.3 已出

  • Sticky slot 再 apply。 LayoutEngine.plan 以前純粹按 z-order 分配 slot,所以任何改動之後再撳同一個 layout 都會全部重新洗牌 — Quads 閂咗一個 window 再開另一個 app,四個 window 全部都會移。而家 plan 會先跑一個 sticky pass:window 現時嘅 frame 同某個 slot rect 差距喺 10 pt 之內(用原有嘅 rectsApproxEqual)就claim 咗嗰個 slot,跟住剩低嘅 window 按 z-order 填剩低嘅 slot,多出嘅照舊 minimize。設計上係 stateless — 冇任何持久化嘅 window→slot map 會過期。Sticky placement 嘅目標係 slot 嘅準確 rect,所以細過 tolerance 嘅偏移會自動修正返;拖過位換咗嘅 window 因為坐喺 slot rect 上面,所以再 apply 都保住新位置;撳另一個 layout 因為冇 frame match 得到,所以照舊全部按 z-order 重排。已知限制:唔肯接受 AX 改 size 嘅 app(Apple 系統設定)永遠唔會準確坐喺 slot rect 上面,所以唔會被當成 sticky。
  • Placement.slotIndex。 Sticky 分配可能會claim 咗 slot {0, 2, 3} 而 slot 1 空住,所以 placement 唔再同 layout.slots index 對齊。Placement 而家自己帶住 slot index,Coordinator.rebuildDragSwapObservers 改用佢嚟砌 lastWindowToSlotIdx(drag-swap 同 seam-resize 都要讀嘅 map),唔再靠 array 位置推。Sticky window 特意留喺 placements 裡面(目標係佢自己現時嘅 frame)而唔係剔走,咁樣佢哋就唔會失去 drag-swap observer。
  • Window style menu panel。 MenuBarExtra 由 .menu 轉去 .window:macOS 原生 menu 撳一下一定會閂,冇得改,所以以前撳 Free Mode 就會閂咗個 menu,要再開一次先撳得到 layout。MenuBarContentView 而家係一個 280 pt 嘅 panel,用自訂 MenuRowButtonStyle 排(圓角 hover highlight、disable 時淡色),內容、次序、thumbnail、hotkey glyph 同所有 localization key 都同舊 menu 一樣 — 冇加新字串。撳 Free Mode / layout / workspace 都唔會閂,checkmark 即時更新;透過捕捉到嘅 hosting NSWindow(PanelWindowAccessor),Settings、onboarding、update alert 同 Escape 可以主動閂佢。
  • Tests 363 → 371。 LayoutEngineStickyPlanTests 八個 case 釘實個 contract:全部再 apply 視覺上唔動、閂一個開一個只有新嗰個移、換位保住、overflow minimize 多出嗰個而唔係 sticky 嗰啲、撳另一個 layout 按 z-order 重排、tolerance 邊界 8 pt vs 12 pt、疊住嘅 window 按 z-order 分先後、underflow 空隙報返正確嘅空 slot 數。Universal(arm64 + x86_64),macOS 14+,Apple notarized。

v0.7.2 已出

  • Liquid Glass 設定視窗(macOS 26 Tahoe)。 SettingsWindowController 加咗 availability-gated 嘅 chrome 設定 — .fullSizeContentView、透明 titlebar、隱藏 title — hosting controller 仲包咗入一個 NSVisualEffectView container(.behindWindow、.underWindowBackground):SwiftUI 嘅 containerBackground(for: .window) 喺手動 host 嘅 NSWindow 入面係唔會生效嘅。Sidebar 變咗系統浮動玻璃 panel,成個視窗 blur 住後面嘅嘢。macOS 14/15 保持不透明視窗。
  • 五個 tab 統一 chrome。 SettingsRoot 加咗共用嘅 DetailTabChrome modifier,五個 tab 而家有一樣嘅頂部標題列,Tahoe 上面仲會清走 scroll background(以前得 Workspaces/Layouts 呢兩個有 toolbar 嘅 tab 先有標題列);WorkspacesTab 個 list 加咗 8pt 頂部 margin,第一行唔會再貼住浮起 panel 嘅頂。
  • 所有 OS 版本一律靠頂。 SettingsRoot 嘅 detail frame alignment 由 center 改做 .top;About 同 Interaction 唔會再垂直置中。
  • Tests 363/363 — 只改 SceneApp,SceneCore 冇郁過。Universal(arm64 + x86_64),macOS 14+,Apple notarized。

v0.7.1 已出

  • 修正 — layout 上下鏡像。 所有 authoring 界面(canvas editor、縮圖、template 嘅 slot 數學)都係由螢幕頂部量度 unit slot 嘅 y,但係 Slot.absoluteRect(in:) 將 unit y 直接放入由底部量度嘅 NS visibleFrame — 所以任何上下唔對稱嘅 layout apply 出嚟都係上下鏡像(自訂嘅「上 2 格、下 3 格」變咗「上 3 下 2」;Main + Side(縱向)個主窗口跌咗落底;lShapeTop/Bottom 對調)。對稱嘅 Halves/Thirds(縱向)presets 由 V0.4 開始一直遮住咗個 bug。而家 y 軸喺 unit→NS 轉換嗰度啱啱好翻轉一次(同 AXWindow.setFrame 單次 NS→AX 翻轉一樣嘅原則),LayoutReflow(seam 拖拽用嘅 NS→unit 逆向數學)都跟住翻轉。
  • 修正 — seam-resize 對 tree layout。 Canvas 整嘅自訂 layout 帶住過時嘅 template / slotProportions 欄位(由複製來源留低);SeamResizeController 攞咗呢啲舊資料餵俾 LayoutReflow,搞到拖拽 resize 任何一個窗口都可能將其他窗口推去另一個 layout 嘅位置 — 邊個窗口郁視乎邊個窗口 echo 咗 resize event,所以睇落好似隨機。而家 Coordinator 嘅 seam context provider 對 tree 形 layout 一律回 nil(自由 resize;reflow 繼續只支援 template)。
  • Tests 357 → 363。 六個新 case 鎖定方向約定:Slot.absoluteRect 上下 anchoring、報告嗰個 5 格情境嘅 regression test(上 2 下 3 要 materialize 返上 2 下 3)、重寫咗嘅 LayoutReflow row 語義(slot 0 = 頂部;佢嘅 seam 係窗口嘅 NS minY 邊)。Universal(arm64 + x86_64),macOS 14+,Apple notarized。
  • 升級提示。 如果你之前為咗遷就舊行為而倒轉咗自己個 layout,更新之後要重新編輯一次 — 而家點畫就點 apply。

v0.7.0 已出

  • 自動化界面。 新增 scene:// URL scheme(SceneCore 嘅 URLRouter + AutomationCommand,經 AppDelegate + AutomationDispatcher 接駁)同 5 個 AppIntents(Activate Workspace、Apply Layout、List Workspaces、Toggle Free Mode、Set Free Mode),仲有 SceneAppShortcuts 畀 Siri 用嘅語句。全部指令經 Coordinator.handleAutomationCommand,會過 AX 權限同 Free Mode gate。URL scheme 喺 macOS 14+ 用得,AppIntents 要 macOS 14.1+。
  • Workspace 每個螢幕獨立 layout。 Workspace 加咗 displayLayouts: [DisplayLayoutAssignment](displayName → layoutID)同 resolvedLayoutID(forDisplay:)。啟動情境時 WorkspaceActivator 會 iterate NSScreen.screens,用新嘅 Coordinator.applyLayout(id:on:) 幫每個螢幕套用對應 layout,冇特別設定就用返主 layoutID。用 decodeIfPresent decode,舊 workspaces.json migrate 得順。
  • 修正 — 拖拽對調用咗舊 snapshot。 window→slot 嘅記錄(Coordinator.lastWindowToSlotIdx)淨係喺 applyLayout 時 refresh,所以第二次連續對調會讀到舊位置 — 搵唔到被移走嗰個窗,兩個窗疊喺同一格(連 seam-resize 都一齊受影響,因為共用同一個 map)。DragSwapController 而家會 emit onSwap callback,Coordinator 每次對調之後即刻更新 snapshot。加咗 regression test testConsecutiveSwapsUseUpdatedSlotMap(RED-checked)。
  • 修正 — Free Mode 撳唔返轉頭。 MenuBarContentView 之前用 @EnvironmentObject 睇 freeMode,但 MenuBarExtra 嘅 menu 唔會穩定咁 re-subscribe,所以撳熄 Free Mode 之後 menu 可能仲顯示緊舊狀態。而家 menu 直接用 @ObservedObject 攞 coordinator(同 menu bar 圖示嗰個 fix 一樣)。
  • 測試 317 → 357。 新增 SceneCore 覆蓋 URLRouter / AutomationCommand、DisplayLayoutAssignment,同兩次連續對調嘅 regression。Universal(arm64 + x86_64),macOS 14+,Apple notarized。

V0.6.1 已出

  • 閒置模式 toggle。 SceneApp 嘅 Coordinator 加咗 @Published var freeMode: Bool。打開咗嗰陣,Coordinator.applyLayout(_:) 同 Coordinator.applyWorkspace(id:) 喺最頂 early-return — menu click 同 Carbon hotkey 兩條路都靠呢一個 gate 擋走(hotkey handler 本身都係 route 入呢兩個 entry point,所以一個 guard 就 cover 晒 ⌘⌃1-9,0 同 ⌘⌥1-4)。TriggerSupervisor 加咗對應嘅 var paused: Bool = false,喺 handleDirectActivation(workspaceID:kind:source:) 入口 check,三個 auto-trigger watcher(monitor / 時間 / calendar)全部 short-circuit,唔會 update cooldown timestamp(避免暫停期間嘅事件「記住」cooldown 影響 toggle 返開之後嘅 fire)。Drag-swap 同 seam-resize 嘅 gate 擺喺 Coordinator.rebuildDragSwapObservers 入面 AX onMove / onResize observer closure 入面(guard let self, !self.freeMode else { return })—— observer 本體唔停,所以 toggle 返開即時恢復,唔使重新 arm。
  • Menu UI。 MenuBarContentView 喺 Layouts section 同 Settings divider 之間加咗個「閒置模式」row。打開咗有個前置 Image(systemName: "checkmark"),閂咗就 14pt Spacer 佔位(同 Workspaces section 嘅 checkmark slot 一樣闊,菜單寬度保持唔變)。Layouts 同 Workspaces 嘅 Button row 加咗 .disabled(coordinator.freeMode),暫停期間兩個 list 都係灰嘅。SceneAppApp.swift 加咗個 private MenuBarLabel view,@ObservedObject 個 coordinator,MenuBarExtra 個 icon 喺 rectangle.3.group(運作中)同 pause.rectangle(暫停)之間切換 —— MenuBarExtra 嘅 label closure 唔會自動 subscribe environment object,所以要明確 @ObservedObject。
  • State lifecycle。 freeMode 只係 in-memory state — 永遠唔寫入 settings.json,冇 schema migration。每次 Setpiece 開機都係 freeMode = false,唔理上次 session 有冇暫停過。Coordinator.freeMode.didSet 將值鏡射落 triggerSupervisor?.paused 處理 auto-trigger gate;optional chain 處理 launch 早期未 configure(triggerSupervisor:) 嗰段窄窗口(UX 上唔可能咁早 toggle,但係 defensive correct)。
  • 多語。 Localizable.xcstrings 新加 menu.free_mode:Free Mode(en)、閒置模式(zh-HK 粵語;用 閒置 唔用 自由,因為粵語 自由 偏向「休閒」嘅意思)、自由模式(zh-TW 書面語)。
  • 冇改 SceneCore;測試維持 317/317。 純 SceneApp glue 改動。SceneApp 暫時冇 swift test 覆蓋 —— 一份 10 步嘅人手 smoke checklist 加咗落 docs/TESTING.md 做驗收(冷啟動係 OFF、hotkey / 拖邊 / auto-trigger 喺 ON 時靜默、dimmed row + ✓ + 暫停 icon、toggle 返開恢復、relaunch 唔保留 state)。

V0.6.0 已出

  • 診斷紀錄子系統(Diagnostics subsystem)。 SceneCore 新加 Diagnostics/ directory:DiagnosticEvent(10 個 case — layoutFired / instant + animated outcome / 7-step workspace step / triggerFired / suppressed / screenDiff / AX permission / app launch timeout / app quit survivors)、EnvironmentSnapshot 配 integer-field ScreenRecord(resolution / visible frame / scale100 / main flag — deterministic SHA prefix signature,整數 round 過避免 float drift)、EventLog 200-entry ring buffer(@unchecked Sendable lock-protected)、DiagnosticWriter actor 包住單一 AsyncStream + 唯一 consumer task(race-safe drain():唔會 per-call site Task {} 走漏,所以 disable 同 queued write 唔會 race)、DiagnosticBudget(newline 安全 truncate、最舊 rotated file 先 evict,2 MB cap,7 日 retention)、GzipWriter(Apple Compression framework + 手寫 gzip header / CRC32 / ISIZE — 用 system gunzip round-trip 驗證)、SaltStore mode 0600 + disable→enable 重新生 salt(forward secrecy)、DiagnosticHasher salted SHA256 prefix=8 → 11-char base64url、Sanitized* DTO(Layout / Workspace / FocusMode / Trigger / Hotkey)+ ExportSanitizer pure transformation。SceneApp 加咗 DiagnosticController composition root、EnvironmentCapture(NSScreen → ScreenRecord)、ProcessRunner protocol、DiagnosticExporter 經 /usr/bin/ditto 包 zip。
  • Hook 散布喺 orchestration 層。 Coordinator.performApplyLayout 紀錄 layoutFired(連 snapshot)+ layoutOutcomeInstant;WorkspaceActivator.activate 每個 stage 都發 workspaceStep(quit / launch / settle / applyLayout / focusOn / setActive / banner)連 timing + appCount + survivorCount;TriggerSupervisor 紀錄 triggerFired 同 triggerSuppressed(reason:cooldown / alreadyActive / inFlight);MonitorTriggerWatcher 每次 didChangeScreenParameters 都記 screenDiff(唔再淨係 connect/disconnect by name);WindowAnimator 統計 per-run setFrameFailures,完成時發 layoutOutcomeAnimated — 包括動畫被中斷嗰個 case,舊 run 嘅 summary 會喺 track 被換之前先 emit。所有 inject 用 constructor default diagnostics: DiagnosticSink = .noop,舊 test code 唔使改。
  • Export bundle + GitHub issue 直通。 About tab 嘅「匯出診斷紀錄畀 bug 報告用」會包 events-*.jsonl(active + 過去 gzip 嘅日子)、sanitized layouts.json / workspaces.json / settings.json、ring buffer tail(in-memory recent events)、system-info.txt(Setpiece 版本、macOS 版本、AX granted、screen 數、hashID = SHA256(salt)[0:8] — 唔包 salt 本身),同 README.txt 解釋 privacy contract。NSSavePanel save 完之後,NSWorkspace.activateFileViewerSelecting 跳出 Finder highlight 個 zip + NSWorkspace.open 開預先填好嘅 GitHub issue(標題 Bug report (Setpiece v<version>)、body 有環境資料、相同 hashID,labels bug,diagnostics)。
  • 持續性 Workspace(V0.7 一齊 ship)。 Workspace 加咗 assignedDesktop: Int?(1-9)、pinnedApps: [String]、同 enforcementMode: WorkspaceEnforcementMode(.off / .arrangeOnly / .hideWhenInactive / .quitWhenInactive)。每 field 都用 decodeIfPresent decode,V0.4 舊 workspaces.json migrate 順暢。DesktopSwitcher 用 CGEvent post 一個合成嘅 Ctrl+N 觸發 Mission Control 「Switch to Desktop N」。WorkspaceAppPolicyEnforcer subscribe NSWorkspace.didLaunchApplicationNotification + didActivateApplicationNotification + workspaceStore.onChange,持續執法 — disallowed = (其他 workspace 嘅 pinned)− (active 嘅 pinned)− (Setpiece 自己嘅 bundle),跟住按 mode .hide() 或者 .terminate()。Activation flow update 為:quit → switchToDesktop → launch(pinned ∪ appsToLaunch dedup)→ settle(如果冇 launch 就 skip)→ applyLayout → focusOn → setActive → banner。
  • Workspace 編輯器 Save UX。 WorkspaceEditorView 帶住個 savedBaseline: Workspace。isDirty = draft != savedBaseline 控制 Save 掣嘅 .disabled(!isDirty);成功 update 之後 promote savedBaseline = draft,Save 即時 disable。justSaved flag 撳完 set 2 秒,畫綠色 checkmark.circle.fill 「已儲存 ✓」label;中途用家再改嘢就即刻清走(避免誤以為 save 咗)。新加 workspace.editor.saved + workspace.editor.unsaved 三個 locale(en / zh-HK 粵語 / zh-TW 書面語)。
  • Settings v2 → v3 migration。 SettingsStore 加 diagnosticsEnabled: Bool(default true),decodeWithMigration 處理 v2 input 時 default 落新 field,第一次讀完即時 re-persist。SettingsStoreViewModel 加 setDiagnosticsEnabled + onDiagnosticsToggle: ((Bool) async -> Void)? callback,AppDelegate 將佢 wire 落 DiagnosticController.enable() / disable() — toggle off 會 drain writer + 刪曬 artifacts + 重新生 salt(下次 enable 時 forward secrecy)。
  • 測試 244 → 317。 新 Diagnostics/ test directory 9 個檔:DiagnosticEventTests(每個 case Codable round-trip、JSON shape {ts, t, p}、per-event byte cap、privacy contract「冇 plaintext leak」property)、EnvironmentSnapshotTests(signature 穩定性 + 對解像度 / Sidecar reconnect 喺唔同 scale / main display 變 / active display 變嘅敏感度)、HashingTests(同一 salt 出同一 hash + 字符集 + 長度 11)、ExportSanitizerTests(layouts + workspaces + triggers PII drop、within-bundle 對得返、cross-salt 對唔返、Codable round-trip)、EventLogTests(200-cap FIFO eviction + observer fire + 1000 線程並發 append)、DiagnosticBudgetTests(檔名 day parsing、newline 安全 truncate、retention by 內嵌 day、total-budget eviction 保留 active jsonl、gzip compress + system gunzip round-trip)、DiagnosticWriterTests(race test:enqueue 100 → drain → 全部入到 disk、日子轉換 rotation、deleteAllArtifacts、寫出嚟嘅 line 係合法 DiagnosticEntry JSON、active file 喺 cap 度 truncate)、GzipWriterTests(用 /usr/bin/gunzip round-trip、RFC 1952 CRC32 vector、重複 input 高壓縮)、SaltStoreTests(file mode 0600、regenerate 出唔同 byte、corrupt file 觸發 regenerate、idempotent delete)。SettingsStoreTests 加 v2 → v3 migration round-trip + 持久化測試。共 73 個新 case。

V0.5.7 已出

  • 用家自訂排列(custom layout build)。 SceneCore 新加個 LayoutNode indirect enum(.leaf / .hSplit(ratio, top, bottom) / .vSplit(ratio, left, right)),承載 binary split-tree,同 11 個固定 LayoutTemplate 並存。CustomLayout 多咗個 optional customTree: LayoutNode? field,Codable 用 decodeIfPresent 實現前後兼容 —— v0.5.6 嘅 layouts.json 照 decode,template-only 嘅 layout 亦唔會寫呢個 key。toLayout() 有 tree 就用 tree 嘅 flatten() -> [Slot](pure function,遞歸切 unit rect),LayoutEngine.plan 唔使改。SceneApp/SceneApp/Settings/CustomLayoutCanvasEditor.swift 係新加嘅遞歸 SwiftUI view —— 撳 leaf 出 menu(橫向拆 / 縱向拆 / 刪除呢格),seam 係可拖動嘅 Rectangle,DragGesture.onChanged 即時更新 ratio binding。LayoutsTab 加咗 "+ Custom" toolbar 掣,插入 customTree = .leaf 嘅新 layout;LayoutDraftEditor 見到 tree 就 route 去 canvas editor。
  • 拖邊同步(seam reflow on drag)。 AXMoveObserverGroup 而家同時 observe kAXResizedNotification 同 kAXMovedNotification。新 SeamResizeController(跟 DragSwapController injected-probe pattern)讀 AX 報嘅新 frame、call LayoutReflow.reflow(template:proportions:slotIdx:newWindowFrame:visibleFrame:) —— pure function 支援 .twoCol / .twoRow / .threeCol / .threeRow,3-way 中間格會自動判斷邊個邊 moved —— 然後用 setFrame 將新 rect 寫去非 initiating 嘅 neighbour。Self-fire guard 靠 mouse-button probe 加 activeInitiatingID latch,Setpiece 自己 setFrame 嘅 echo 唔會 re-enter。共用 DragSwapConfig.enabled toggle;custom-tree layout 暫未 reflow(留到 v0.6)。
  • Dirty-state Save 掣 + save-persist bug 修補。 LayoutDraftEditor(今個 release 新 introduce,本地 @State draft wrapper 包住 LayoutEditorView 同 CustomLayoutCanvasEditor)帶住個 lastSaved snapshot。canSave = draft != lastSaved 傳落去兩個 sub-editor,綁喺 Save 掣嘅 .disabled(!canSave);成功 store.update 之後更新 lastSaved,Save 即時變暗。Cancel revert 去 lastSaved(唔係 original load),save → edit → cancel 會返去上次 save 嗰個狀態。另外 editor 而家用 proper @State draft + 明確 commit —— 修咗之前 .onChange(of: draft.template) 喺 sidebar 切換嗰陣誤觸(bindings 轉去第二個 layout,誤以為 template 改咗),悄悄將 template default proportions 寫返 store,洗走用家自訂 proportions 嘅 bug。
  • 多 screen AX ↔ NS coordinate 修補。 新加 Sources/SceneCore/Display/DisplayCoordinates.swift 集中 AX(全局 top-left)↔ NSScreen(每個 screen 各自 bottom-left)轉換。AXWindow.frame 而家返 NS-flipped frame;AXMoveObserverGroup 個 observer callback 喺 invoke onMove / onResize 之前 flip 一次;AXWindowEnumerator 個 center-in-visibleFrame check 都 route 去共用 helper。全部下游(drag-swap、seam resize、plan/apply)喺同一個 coordinate system 入面運作。第二個 screen 放喺主 screen 下面或者左邊嘅 setup,而家 seam target 計得正,window 唔會再飛出 screen 外面。
  • Tests 177 → 244。 LayoutReflowTests(20)cover 4 個支援模板、middle-slot 判斷、clamp 去 [0.1, 0.9]、visibleFrame 有 offset(多 screen)、唔支援嘅模板返 nil、退化 input。SeamResizeControllerTests(12)cover 核心 reflow、self-fire guard(mouse / option / disabled / echo)、nil / 未知 window short-circuit、3-way smoke、mouse-up state reset。LayoutNodeTests(17)cover slot-count、跨 split flatten、5 格示例逐字 check、100% 覆蓋 invariant、negative / >1 / NaN ratio 嘅 clamp、Codable round-trip 同 on-disk JSON shape 穩定性。CustomLayoutTests 加 5 個新 case(customTree 覆蓋、template fallback、round-trip、冇 customTree 嘅 legacy JSON decode、冇 customTree 時 encode 唔會寫個 key)。DisplayCoordinatesTests cover flip helpers。

V0.5.6 已出

  • In-app update installer。 Menu 入面「有新版本」而家會出個確認 alert,三個掣:「安裝並重開」(default)、「睇 Release Notes」(開 GitHub release page,即係 V0.5.6 之前嘅行為)、「遲啲先」。撳「安裝並重開」之後,UpdateInstaller 會 download 新 DMG 去 ~/Library/Caches/Scene/、用 codesign -dv 驗簽 + 確認 TeamIdentifier=22K6G3HH9G、寫個 detached helper bash script 入 /tmp/、用 nohup launch 佢、然後 quit Setpiece。Helper 會等 Setpiece PID exit、mount DMG、用 ditto --noqtn 換咗已裝嘅 bundle(用 Bundle.main.bundleURL 攞返路徑,所以唔係裝喺 /Applications 都得)、剝走 destination 嘅 com.apple.quarantine xattr、refresh LaunchServices、unmount、刪 cache、再 open 新 Setpiece。
  • Accessibility 授權跨版本保留。 TCC 將授權綁喺 binary 嘅 Designated Requirement 度(anchor apple generic and identifier "com.hillman.SceneApp" and certificate ... and certificate leaf[subject.OU] = "22K6G3HH9G")。呢個 requirement 喺同一個 Developer ID 簽嘅 release 之間係穩定嘅。ditto 保留 com.apple.macl xattr — TCC 嘅 per-file pointer,將授權錨定喺呢個 bundle 度。新 install 開機嗰陣 AXIsProcessTrusted 已經係 true:唔使重新 grant、唔使行 tccutil reset、唔使去 System Settings 㩒掣。
  • 裝失敗會 rollback。 Helper 喺 ditto 之前先 backup 舊 app 去 /tmp/Scene.app.bak-$$。如果 ditto 失敗就 restore backup 同 unmount DMG。Log 寫去 /tmp/scene-update-<pid>.log 方便事後 debug。
  • Bootstrap 注意。 V0.5.5 個 menu 仲係淨係開 GitHub page 嘅舊行為,所以 V0.5.5 → V0.5.6 仲係要手動裝一次。之後 V0.5.6 → V0.5.7+ 就行 in-app installer。
  • SceneCore 冇改;測試仲係 177/177 全通過。 新 file SceneApp/SceneApp/UpdateInstaller.swift(~150 行),加埋 UpdateChecker 幾行 surface GitHub release assets[] 嘅 DMG URL。

V0.5.5 已出

  • Quit + relaunch 即時撳 GitHub check 新版本 — UpdateChecker.startPeriodicChecks() 喺 launch 時而家會 bypass 24 小時 lastCheckedAt 冷卻,靠新嘅 forceCheck() helper。之前如果 Setpiece 喺 v0.5.3 已經 cache 咗 lastCheckedAt(嗰時 v0.5.4 仲未 publish),而你喺 24 小時內 quit 再 relaunch,就會悄悄 skip 咗 GitHub call,menu bar 完全唔會見到「有新版本」。每個鐘嘅 background timer 同 wake-from-sleep 仍然守 24 小時冷卻 — 嗰啲 fire 唔需要用家 intent,要保護 GitHub rate limit(unauth 60 req/hr per IP)。
  • Workspaces tab toolbar 加返 delete 掣 — WorkspacesTab toolbar 而家有 trash 掣,同 LayoutsTab 一樣。之前淨係靠 List.onDelete swipe gesture,但係 macOS NavigationSplitView 上幾乎冇人發現呢個手勢,用家以為四個 seeded workspace(Coding、Meeting、Reading、Streaming)係冇得 delete。Inline-swipe 保留做 secondary affordance。
  • SceneCore 冇改;測試仲係 177/177 全通過 — 純 SceneApp UI / 生命週期修補。

V0.5.4 已出

  • Welcome window 唔再等 Accessibility 先彈 — 一次性 welcome 視窗以前要等 AXIsProcessTrusted 為 true 先彈。任何由 ad-hoc-signed v0.4.x 升上 notarized v0.5+ 嘅用家,因為 cdhash 唔對 TCC 失效(toggle 仲 ON 但 AXIsProcessTrusted 永遠 false),結果 welcome 永遠唔出。新流程:welcome 第一次 launch 即時彈,唔理 AX 狀態,由 FirstLaunchWindowController.afterDismiss 喺用家 dismiss 之後自動接續 onboarding(如果 AX 仲未授權)。
  • AX 救援指令一開始就睇到 — OnboardingView 唔再將 tccutil reset Accessibility com.hillman.SceneApp 嗰段(連埋複製掣同貼到 Terminal 嘅步驟)gate 喺撳「再檢查」失敗一次先出。大部分用家喺 System Settings 撳着之後等 2 秒輪詢自動偵測,根本冇撳「再檢查」,所以一直冇睇過救援指令。而家一開就見。
  • 返返嚟嘅用家如果未授權,自動彈 onboarding — AppDelegate.applicationDidFinishLaunching 而家會喺 welcome flag 已 set 嘅情況下,calling openOnboardingIfMissingAX()。咁 TCC 壞咗嘅用家就唔使再喺 menu bar 度搵收埋嘅「Grant Accessibility」。
  • SceneCore 冇改;測試仲係 177/177 全通過 — 純 SceneApp UI / 生命週期修補。

V0.5.3 已出

  • Native app 動畫升到 60Hz — WindowAnimator 而家按動畫嘅 window 組合揀 AX throttle:任何一個 window 屬於已知 Electron bundle ID(VS Code、Cursor、Chrome、Brave、Slack、Discord、Figma、Notion、Obsidian、Teams)就維持 30Hz;全部 native 就升 60Hz。native app 喺同一段動畫時間內大約兩倍幀數,ProMotion 屏幕上感覺明顯順啲。
  • Duration 按距離 scale — 你設定嘅 durationMs 而家當係 ~600pt 對角線移動嘅參考值。短距離縮到 ~70%,長距離拉到 ~140%,兩頭 clamp。AnimationConfig 自己嘅 [100, 500]ms 做最後 safety net。
  • AX dedup 更 tight — 每個 window 嘅 dedup tolerance 由 0.5pt 降到 0.2pt,拎返少少喺 easeOut 尾段被食咗嘅幀。
  • Universal binary(Intel 支援) — scripts/build-dmg.sh 而家出 arm64 + x86_64 兩個 slice。一個 DMG 裝晒,Apple Silicon 唔使行 Rosetta。macOS Tahoe (26) 係最後一個支援 Intel 嘅版本,呢個係 Intel reach 嘅最後窗口。
  • Deployment target — MACOSX_DEPLOYMENT_TARGET 回到 14.0(Xcode 之前喺 build 機嘅 macOS 26.4 下靜雞雞將其改咗做 26.4)。
  • Xcode 26 workaround — build-dmg.sh 強制 SWIFT_OPTIMIZATION_LEVEL=-Onone,因為 Xcode 26 Release optimizer 喺 pre-26 deployment target 會喺 SIL pipeline crash。對 menu bar app 影響可以忽略;Apple 出咗修好嘅 toolchain 之後就可以拎走。
  • 177 個 SceneCore unit test 不變 — 三個 smoothness 改動全部喺 WindowAnimator(SceneApp bridge)入面。

V0.5.2 已出

  • 首次啟動歡迎畫面 — 新 user Accessibility 授權完第一次開 app 會見到歡迎窗口,內有 SwiftUI 畫嘅 menu bar mock + 「打開設定」shortcut。用 UserDefaults.hasShownFirstLaunchWelcomeV1 守,彈出嗰刻就 set(唔係 dismiss 時),即使 ⌘Q 中途 quit 都唔會再彈。Settings → About → 「再睇歡迎畫面」可以手動再彈。
  • Dynamic 版本號 — About tab 由 CFBundleShortVersionString 直接讀 bundle,唔再靠 String Catalog 常數,將來 bump version 唔使改 catalog。

V0.5.1 已出

  • 持續 update detection — UpdateChecker 而家每個鐘撳一次 GitHub(跨 device 仲係 rate-limited 一日一個 network call),加埋 wake-from-sleep 會 trigger,長開機嘅 Mac 一個鐘內就見到新版本,唔使等下次 launch。

V0.5.0 已出

  • Developer ID signing + Apple notarization — Setpiece 而家 Apple notarized。Gatekeeper 接受裝,冇「cannot be verified」提示。Homebrew cask 嘅 quarantine strip 仲照跑但已經多餘。
  • TCC Accessibility 授權跨版本保留 — notarization 穩定咗跨 build 嘅 signing identity,所以 TCC cdhash binding 唔再每次新 DMG 都失效。V0.5.x → V0.5.y 升級維持授權。

V0.4.3 已出

  • Passive update nudge — 新加嘅 UpdateChecker 每 24 小時撳一次 GitHub releases/latest(per-device debounce 擺 UserDefaults),遠端 tag 新過 CFBundleShortVersionString 就喺 menu bar 頂顯示 tinted 嘅「Update available: v%@ — 撳呢度 download」。撳落去開 release page,user 自己 install DMG。
  • 特登唔用 Sparkle。 Setpiece ad-hoc sign,每次 build 個 cdhash 都變。Silent auto-install 會每次 update 都令 Accessibility 授權失效,核心功能就冚。留返 DMG 安裝 path,Homebrew tap 個 postflight(清 quarantine)先會照跑。
  • isVersionTag(_:newerThan:) 搬咗入 SceneCore 做 public free function(Sources/SceneCore/Settings/VersionCompare.swift)。UpdateChecker 只留 I/O 同 state;semver 邏輯唔使 Bundle / URLSession / Xcode 都 test 到。
  • 加咗 19 個 unit test 覆蓋 patch / minor / major bump、v prefix、zero-padding 相等(1.2 == 1.2.0)、trailing zero、pre-release suffix fail-safe、空字串 + garbage input — 所有 malformed input 返 false,唔會誤撳 nudge。
  • Icon 重新壓縮 — 9 個 AppIcon PNG 之前 uncompressed 入返 working tree,重新跑 pngquant 拎返 v0.4.1 嘅大小(~672 KB → 156 KB 總和)。
  • SceneCore test 數目:158 → 177。

V0.4.2 已出

  • ProMotion(120Hz)動畫 lag — WindowAnimator 而家 throttle AX 寫入到 30Hz,加埋每個 window 0.5pt 範圍內 dedup。兩個 Electron app(Cursor、Chrome、VS Code)swap 嗰陣,原本 250ms 嘅動畫唔再拖成秒以上 IPC back-pressure。
  • Workspace「一開就預選咗」 — activeWorkspaceID 改做 session-only state。由 WorkspaceStore 嘅 disk model 抽走,之前 session 嘅 activation 唔會再喺下次開 app 出現一個用戶冇揀過嘅剔號。舊 disk 嘅 activeWorkspaceID key 會被 JSONDecoder 預設 policy ignore,下次寫入時清走。
  • 即時 Workspace click — AppLauncher.quit 空 bundleIDs list 即刻 return,唔食 5 秒 gentle-quit grace;WorkspaceActivator 空 appsToLaunch 就跳過 1.5 秒 settle。Default seed workspace click 完 ~50ms 就出 layout,唔再係 ~6.5 秒。
  • Layout 再撳一次捉到新開嘅窗口 — Coordinator.applyLayout 記住上次 apply 嘅 layout ID 同時間。5 秒內再撳同一個 layout,enumerate 前加 200ms settle,畀 CGWindowListCopyWindowInfo 時間登記剛開嘅新窗口。第一次撳 layout 嘅 latency 冇變。
  • 158 個 SceneCore unit test — testSetActivePersists rename 做 testActiveWorkspaceIsSessionOnly,assertion 反過嚟;總數冇變。

V0.4.1 已出

  • DMG size 2.8MB → 1.4MB — Swift -Osize flag、LZFSE DMG 壓縮、加 pngquant 壓 app icon。靚仔安裝視窗:自訂背景圖、方向箭頭、icon 位置 pin 死、收埋 toolbar/sidebar。
  • Workspace activation 穩定性 — WorkspaceActivator 套 layout 失敗唔會再誤報「已啟動」;TriggerSupervisor single-flight guard 防止 manual + auto 啟動同時 race;日曆 keyword 空唔會再誤撞所有 event。
  • 重裝授權指引 — 重新 build 令 TCC cdhash 綁定失效嗰陣,onboarding 視窗直接畀 tccutil reset Accessibility com.hillman.SceneApp 指令同一個「複製」按鈕,唔再試唔可靠嘅「toggle OFF/ON」做法。
  • WorkspaceStore.insert 防重複 ID guard — 擲 .duplicateID(id:),唔再靜悄悄 overwrite。

V0.4.0 已出

V0.4 — Workspaces + layout thumbnail + 縱向 seed + 多語 UI

  • Workspaces(頭條)— bundle layout + 要開 app + 要閂 app + Focus mode + auto-trigger。4 個 seed(Coding / Meeting / Reading / Streaming)綁 ⌘⌥1–4。撳一下就 activate 成個 context。
  • 5 種 trigger — .manual(hotkey / menu)、.monitorConnect(displayName)、.monitorDisconnect(displayName)、.timeOfDay(weekdayMask, hour, minute)、.calendarEvent(keywordContains)。First-match-wins 排序、每個 Workspace 30 秒 cooldown(auto-trigger 用;manual 跳過)。
  • 7 步 activation — previous Focus Off → quit(gentle + 5 秒 grace + notify-on-fail;唔 force-terminate)→ launch parallel → settle 1.5 秒 → applyLayout → Focus On → setActive + banner。
  • Layout thumbnail — menu bar 同設定度每個 layout reference 都即時 render slot 比例 preview。3 個尺寸 reuse:24×16(menu)、32×22(Layouts tab)、40×26(Workspace editor)。
  • 3 個縱向 preset seed — Main + Side Vertical(⌘⌃8)、Halves Vertical(⌘⌃9)、Thirds Vertical(⌘⌃0)。透過 V0.2 嘅 applyFutureSeeds 機制 migrate 落現有 install(呢個機制嘅第一次實戰)。
  • Default hotkey 重新編排 — 10 個 layout seed 全部由 ⌘⇧1-7 改做 ⌘⌃1-9,0,避開 macOS ⌘⇧3/4/5 截圖。用戶自設嘅 hotkey upgrade 時保留。
  • 多語 UI — Localizable.xcstrings 入面 133 條 key。Ship 時 English + 繁體中文(港)full parity;部分支援 繁體中文(台)+ 日本語(冇嘅 key fall back en)。
  • 跨 store hotkey 衝突 — Workspaces 同 Layouts 共用一個 chord namespace;兩個 store 嘅兩階段 setHotkeyConflictProbe setter 令 block-save 嘅 error message 顯示係邊個 resource 佔住個 chord。
  • 41 個新 SceneCore unit test(總 157,V0.3 係 116)— Workspace / WorkspaceTrigger / FocusModeReference Codable、WorkspaceSeeds、WorkspaceStore CRUD + 衝突 + seed migration、WeekdayMask OptionSet。

V0.3 — Drag-to-swap

  • 拖任何 placed window 落另一個位就 swap。Source snap,被換走嗰個行 V0.2 動畫引擎(250ms easeOut,跟 Animation tab 設定)。
  • Interaction tab — 開 / 閂 drag-to-swap;drag 距離 threshold 可調(10–100pt)。
  • 逃生口 — 揸住 ⌥ 拖 free-move(唔 swap);drag 中途撳 Esc cancel,snap 返原位。
  • App 層 AXObserver bridge(AXMoveObserverGroup、AXWindowLookup、DragSwapAnimationSink)將 SceneCore 嘅 DragSwapController(V0.1 已經寫好)wire 入實際窗口。
  • 加咗 24 個 SceneCore test(92 → 116)。

V0.2 — 自訂 + smooth animation

  • 11 種 grid template — single、2-col/3-col、2-row/3-row、2×2/3×2 grid、加 4 種 L-shape。每種用 slider 編比例。
  • 自訂 layout — save / rename / delete;persist 做 JSON 入 ~/Library/Application Support/Scene/layouts.json。V0.1 嘅 7 個 preset 變成可編輯 seed(UUID 穩定)。
  • 每個 layout 自訂 hotkey 加 block-save 衝突(一個 chord 只屬一個 layout)。
  • Smooth animation — position+size 同步 interpolate。可調:enable toggle、duration 100-500ms、easing(Linear / Ease Out / Spring)。7+ window 自動 fall back instant。
  • 設定視窗 — SwiftUI sidebar nav。V0.4 tab 次序:Workspaces / Layouts / Hotkeys / Interaction / About。
  • AX permission fix — forceRecheck() 用 AXIsProcessTrustedWithOptions(prompt: false) 解 running-process trust cache trap。
  • 加咗 66 個 SceneCore test(26 → 92)。

V0.5+ — 候選

高優先

  • Per-display 獨立 layout — v0.7.0 同 v0.8.2 部分做咗:Workspace 可以幫每個螢幕分配 layout(Workspace.displayLayouts),啟動時套用;由 v0.8.2 開始,拖拽對調同接縫調整可以同時喺每個螢幕運作(PlacementRegistry)。仲未做:standalone(唔經 Workspace)嘅每個螢幕 layout,同埋將一個窗放落另一個螢幕嘅格度,就加入嗰個螢幕嘅 layout(而家淨係喺嗰度放低)。
  • Launch at Login — 而家冇 UI,用戶手動加入 Login Items。About 或 Workspaces tab 加個 toggle 就 close loop。

中優先

  • Pattern learning — 觀察用戶手動 setFrame 嘅 pattern,surface 建議:「我見你下午通常 70/30 Cursor + Chrome,要唔要 save 做 preset?」Local-first;optional LLM pass,用戶 BYOK API key。
  • AI / 自然語言 layout input — 新 hotkey(建議 ⌘⇧Space)開個細 input。打「cursor 左 chrome 右」或者粵語版,LLM parse → layout JSON → 透過 Coordinator.applyLayout apply。
  • Per-app rule — 「Slack 永遠入 slot 4,唔理 user 撳邊個 preset」。
  • Aggressive quit toggle — opt-in 嘅 per-Workspace flag,5 秒 grace 之後 forceTerminate 仲活住嘅 app。預設 off,保留 V0.4「永遠唔破壞未儲存 work」嘅政策。
  • In-app 語言 switcher — V0.4 只靠系統 locale。畀用戶喺 app 入面揀 language,唔使改 macOS locale,係細但實用嘅加嘢。

低優先 / stretch

  • Free-form canvas drag layout editor — EpycZones 嗰種。V0.2 特登揀「grid template + slider」呢個 constrained mechanic,animation path 乾淨啲、Settings UI 細啲。Canvas drag 可以同時 ship,唔需要取代。
  • Stage Manager awareness — detect + 尊重 Stage Manager group。
  • 多啲 Workspace trigger — location / network SSID / audio device / Bluetooth。每個都加多個 permission prompt + watcher,有需求先加。
  • Homebrew Cask submission(要 notarize 先)。
  • 跨 Mac iCloud sync Workspaces + Layouts。
  • Workspace marketplace / 社群分享。
  • Touch Bar / external display widget 快速 activate Workspace。

Non-goal

  • Mac App Store 分發。要開 App Sandbox,禁止控制其他 app 窗口。Rectangle、Magnet、EpycZones 等喺 Store 以外嘅原因都一樣。
  • iOS / iPadOS port。冇對應 API。
  • 跨平台 port。全部 code 靠 macOS 特有 API。
  • 完整 automation / Shortcuts 取代。Workspaces trigger 集中 5 種;複雜 automation 去 Shortcuts.app。

Clone this wiki locally