
前情提要
寫部落格、回 Issue 的時候,常常需要錄一小段操作畫面。macOS 內建的 QuickTime(或 ⌘⇧5)錄出來是 .mov,每次都還要再丟去別的工具轉 GIF、調尺寸,非常麻煩。
所以我想要一個很單純的小工具:
介面像 QuickTime 的「新增螢幕錄影」,可以錄整個桌面或框選範圍,錄完直接產生 GIF,而且可以選 GIF 的解析度。
剛好上一篇第一次用 Herdr 用得很順,這次就把規模再拉大一點:
- Claude Code 當 PM:開 remote GitHub repo、寫 SPEC、開 Issue、review、commit、push。
- agy 當 coder,而且一次開兩個 Pane:同時解兩個 Issue。
- 每個 coder 做完都要回報給 Claude Code,review 通過才會 commit。
成果在這裡:kkdai/record2gif,從開 repo 到 GIF 成功存到桌面,大概一天。
一、record2gif 做了什麼?
| 功能 | 說明 |
|---|---|
| QuickTime 風格控制列 | 小小一條浮動視窗:錄整個螢幕 / 錄選取範圍、Options、紅色 Record 按鈕 |
| 框選範圍 | 全螢幕半透明遮罩,拖一個方框,顯示像素尺寸,可以調整後再錄 |
| 錄影中 | 控制列自動隱藏(不會被錄進去),選單列顯示 ⏹ + mm:ss,點一下或按 ⌃⌘Esc 停止 |
| 匯出視窗 | 循環預覽、解析度(原尺寸 / 1280 / 960 / 640 / 480 / 320 px / 自訂寬度,維持比例、絕不放大)、影格率(5–30 fps)、無限循環開關、預估輸出尺寸 |
| 存檔 | 預設存到桌面,檔名 Recording 2026-11-14 at 15.13.43.gif,有進度條、可以取消、存完可以在 Finder 顯示 |
技術選擇刻意保持簡單,完全沒有第三方套件:
| 部分 | 技術 |
|---|---|
| 專案 | Swift Package Manager(沒有 .xcodeproj),macOS 14+ |
| UI | SwiftUI + AppKit(NSPanel、NSStatusItem、遮罩視窗) |
| 錄影 | ScreenCaptureKit(SCStream)→ AVAssetWriter 寫 H.264 .mov 暫存檔 |
| 轉 GIF | AVAssetReader 逐格解碼 + ImageIO(CGImageDestination)編碼 |
| 全域快捷鍵 | Carbon RegisterEventHotKey(不需要輔助使用權限) |
| 測試 | Swift Testing,19 個測試 |
錄影時一律用原生像素解析度錄(points × scale),縮小只在匯出 GIF 的時候做,所以同一段錄影可以匯出好幾種尺寸。
二、PM 的工作:先寫 SPEC,再拆 Issue
兩個 coder 平行寫程式,最怕的就是「各寫各的,最後接不起來」。所以 Claude Code 第一件事不是開 coder,而是寫一份 SPEC.md,裡面最重要的是兩件事:
- 固定的目錄結構:哪個 Issue 只能動哪個資料夾(例如只有 #4 可以改
Recording/、只有 #5 可以改Export/)。 - Issue 之間的介面合約:先把
ScreenRecorder、GIFExporter、GIFSizing的 public API 直接寫在 SPEC 裡。
public final class GIFExporter: Sendable {
public init()
public func export(video: URL, to output: URL, options: GIFExportOptions,
progress: @escaping @Sendable (Double) -> Void) async throws
}
有了合約,做 UI 的 coder 不用等錄影引擎寫完,就知道要呼叫什麼。
接著拆成 5 個 Issue、分三輪:
| # | Issue | 相依 | 輪次 |
|---|---|---|---|
| 1 | 專案骨架、App shell、打包 script | – | 第 1 輪 |
| 2 | ScreenCaptureKit 錄影引擎 | 1 | 第 2 輪(Pane A) |
| 3 | GIF 匯出(解析度 / fps) | 1 | 第 2 輪(Pane B) |
| 4 | QuickTime 風格錄影 UI | 1, 2 | 第 3 輪(Pane A) |
| 5 | 匯出視窗:解析度選擇、預覽、存檔 | 1, 3 | 第 3 輪(Pane B) |
三、兩個 agy Pane 平行開發
1. 每個 coder 一個 git worktree
兩個 agy 同時在同一個資料夾改檔案一定會打架,所以每個 Issue 都開一個獨立的 git worktree / branch,coder 只在自己的 worktree 裡工作:
git worktree add ../record2gif-wt/issue-2 -b issue-2
git worktree add ../record2gif-wt/issue-3 -b issue-3
2. 開 coder Pane
上一篇學到的 agy 啟動方式這次直接沿用:
hc run --bg --outlive-parent --name coder-a --cwd ../record2gif-wt/issue-2 \
agy --dangerously-skip-permissions -i "$TASK"
herdr-app do terminal.keep --terminal <ID> --place right # 第一個放右邊
herdr-app do terminal.keep --terminal <ID> --place down # 第二個疊在下面
畫面就變成:左邊 Claude Code(PM),右邊上下兩個 agy。

幾個這次的小變化:
| 項目 | 說明 |
|---|---|
-i |
agy "prompt" 會直接 exit 2,要用 agy -i "..." |
--dangerously-skip-permissions |
上一篇是寫 driver script 逐條放行權限;這次 coder 都關在自己的 worktree 裡、而且不能 commit/push,所以我直接選擇跳過權限,不然兩個 Pane 一直卡在權限對話框 |
| Trust 對話框 | 新資料夾第一次開 agy 還是會問要不要信任,hc keys <t> enter 處理 |
| 等回報 | hc wait --children any:哪個 coder 先做完就先 review 哪個 |
3. Review 流程
sequenceDiagram
participant PM as Claude Code (PM)
participant A as agy Pane A
participant B as agy Pane B
participant GH as GitHub
PM->>A: Issue #2 + SPEC 路徑 + worktree
PM->>B: Issue #3 + SPEC 路徑 + worktree
par 平行實作
A->>A: 實作 + swift build + swift test
and
B->>B: 實作 + swift build + swift test
end
B->>PM: hc send:完成 #3
PM->>PM: git diff、swift test、對照 SPEC
PM->>B: 修改意見(如果有)
A->>PM: hc send:完成 #2
PM->>GH: merge 到 main、commit、close Issue
hc wait --children any 的好處是:A 還在寫的時候,PM 就可以先 review B,不會互相卡住。
四、make run 之後才是真正的開始
5 個 Issue 都合併、19 個測試全過,make app 也打包出 build/record2gif.app。結果我實際按下去:
| # | 現象 | 根因 |
|---|---|---|
| 6 | 控制列視窗變成 900×450 的大白框 | SwiftUI Window 預設尺寸 |
| 7 | 按下 Record,App 直接消失 | 最後一個視窗被藏起來,SwiftUI 自動結束 App |
| 8 | 螢幕錄影權限怎麼勾都沒用 | ad-hoc 簽章每次 build 都變 |
| 9 | 錄影中選單列沒有秒數 | 物件連接順序的 race |
| 10 | GIF 匯出失敗 | 螢幕錄影是可變影格率影片 |
測試全過,跟「真的能用」差很遠。 這 5 個 bug 一樣開 Issue、交給 coder 修、PM review。下面一個一個講。
Bug #6:控制列變成 900×450
SwiftUI 的 Window 場景如果沒特別設定,會給一個預設大小。QuickTime 那種小小一條的控制列,要加:
Window("record2gif", id: "main") { ... }
.windowResizability(.contentSize)
讓視窗貼著內容大小。
Bug #7:按 Record,App 就「當掉」了
我一開始以為是 crash,但 ~/Library/Logs/DiagnosticReports 裡沒有任何 .ips。去看 unified log 才發現,App 是自己正常結束的。
小提醒:在我的 zsh 裡
log被 shell 蓋掉了,要用/usr/bin/log show --predicate 'process == "Record2Gif"'。
原因是:錄影時要把控制列藏起來(不然會被錄進去),而只有一個 Window 的 SwiftUI App,最後一個視窗不見時會自動結束。解法是在 AppDelegate 裡關掉這個行為:
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
false
}
這顆雷其實是 PM 自己埋的:在 review #4 的時候,Claude Code 要求 coder 把 WindowGroup 改成單一 Window(避免開出多個控制列),卻沒想到這個副作用。
Bug #8:螢幕錄影權限設了等於沒設
這個是整個專案最煩的一個。在「系統設定 → 隱私權與安全性 → 螢幕錄製」裡把 record2gif 打開,重新 build 之後又沒權限了。
原因是:macOS 的 TCC 權限是綁在 App 的程式碼簽章上(Designated Requirement)。一開始打包 script 用 ad-hoc 簽章(codesign -s -),每次 build 出來的 CDHash 都不一樣,macOS 就當成「另一個 App」。
解法是建一張本機自簽的程式碼簽章憑證,scripts/build-app.sh 有找到就用它簽:
# 一台機器只要做一次,完整指令在 README
# cert.conf 設定 CN = record2gif Local Signing、extendedKeyUsage = codeSigning
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -config cert.conf ...
openssl pkcs12 -export -legacy ... -out cert.p12
security import cert.p12 -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign
# 清掉舊的 ad-hoc 紀錄、重新打包、授權一次
tccutil reset ScreenCapture com.kkdai.record2gif
make app && open build/record2gif.app
之後 Designated Requirement 會固定成 identifier "com.kkdai.record2gif" and certificate leaf = H"...",再怎麼 rebuild 權限都還在。打包 script 也會印出 codesign -dr - 的結果,方便確認。
Bug #9:選單列沒有秒數
錄影中選單列應該要顯示 ⏹ 00:12 這樣的秒數。程式碼看起來都有寫,但就是不會出現。
原因是 AppDelegate 裡兩個物件的連接順序:AppModel 在錄影狀態改變時會去通知 StatusItemController,但通知的當下,statusItemController 還沒被設定進去,訊息就丟掉了。修法是在兩邊的 didSet 都互相連接,不管誰先建立都會接上,並且讓 phase 的 didSet 跟 recorder.$elapsed 成為更新選單列的唯一來源。
Bug #10:GIF 匯出失敗
最後一關,錄完按 Save GIF,跳出:
GIF export failed: The operation couldn't be completed. (Record2GifCore.GIFExportError error 1.)
先抱怨一下錯誤訊息本身:error 1 對使用者完全沒有意義,所以順手讓 GIFExportError 實作 LocalizedError,給可讀的訊息。
真正的原因是:ScreenCaptureKit 錄出來的是可變影格率(VFR)影片。畫面沒變化的時候它就不送新的 frame,所以 frame 的時間戳是不規則的。原本用 AVAssetImageGenerator.images(for:) 一次要一批固定時間點的畫面,碰到 VFR 影片就會失敗。
改成用 AVAssetReader 從頭到尾依序解碼,對每個 GIF 時間點 t = i / fps,取「時間戳 ≤ t 的最後一個 frame」:
flowchart LR
V["VFR .mov<br/>frame 時間不規則"] --> R["AVAssetReader<br/>依序解碼 BGRA"]
R --> P{"每個 GIF 時間點 t = i/fps<br/>取 PTS ≤ t 的最後一個 frame"}
P --> S["縮放到選擇的解析度<br/>(不放大、偶數尺寸)"]
S --> G["CGImageDestination<br/>寫入 GIF"]
這樣不管原始影片 frame 多不規則,輸出的 GIF 都是固定影格率。也補了 VFR 的測試。
這顆雷也是 PM 埋的:在 review #3 的時候,Claude Code 建議 coder 改用批次的 images(for:) API,當時的測試影片都是固定影格率,所以測試全過。
改完之後:「可以了,GIF 成功存到桌面」 🎉
五、PM 也會出錯
回頭看這 10 個 Issue,有兩個 bug(#7、#10)其實是 PM 在 review 時提的修改意見造成的。這點我覺得很值得記下來:
- Reviewer 的意見也需要驗證。上一篇說「Reviewer 的結論也要被 Review」,這次是反過來:Reviewer 提的修改也會引入 bug。
- 單元測試抓不到這兩種問題。App 生命週期(最後一個視窗關掉)跟真實錄影檔的特性(VFR),都要實際跑起來才會發現。
- 所以人一定要按一次。這 5 個 bug 全部都是我實際操作時發現的,Agent 之間的 review 再仔細也取代不了。
六、平行多 Agent 開發 Checklist
- 1. 先寫 SPEC,把介面合約寫死:public API、目錄結構、哪個 Issue 能改哪個資料夾。
- 2. 照相依關係分輪:同一輪的 Issue 彼此不能相依。
- 3. 每個 coder 一個 git worktree:避免同時改同一個資料夾。
- 4. coder 不 commit、不 push:由 PM review 後統一處理,這也是可以放心
--dangerously-skip-permissions的前提。 - 5.
hc wait --children any:誰先做完先 review 誰。 - 6. 測試全過之後,一定要實際打包、實際操作一次。
- 7. macOS 需要權限的 App,第一天就處理簽章:不然每次 rebuild 都要重新授權。
- 8. 錯誤型別要實作
LocalizedError:不然使用者只會看到error 1。 - 9. Reviewer 的修改意見也可能是 bug 來源:特別是牽涉到框架行為(App 生命週期、影片格式)的建議。
小結
- SPEC + 介面合約是平行開發的關鍵:兩個 agy Pane 同時寫錄影引擎跟 GIF 匯出,合併的時候幾乎沒有衝突。
- Herdr 讓「一個 PM、兩個 coder」看得見:左邊 PM 派工、review,右邊兩個 coder 各自在做什麼即時看得到。
- 「測試全過」只是開始:真正按下去才冒出 5 個 bug,其中 2 個還是 PM 自己 review 時埋的。
- macOS 的眉角:螢幕錄影權限綁程式碼簽章、只有一個視窗的 SwiftUI App 會自己結束、ScreenCaptureKit 錄出來是 VFR。
record2gif 已經開源在 kkdai/record2gif,make app 就能自己打包。之後部落格裡的操作 GIF,應該都會用它來錄了。
- Herdr (2) ,
- agy (4) ,
- Antigravity (6) ,
- Claude Code (11) ,
- macOS (6) ,
- SwiftUI (2) ,
- ScreenCaptureKit (2) ,
- GIF (1) ,
- Multi-Agent (3) ,
- Swift (5)