PAIRWIN 專案簡報 · 2026-08-12

視窗拉攏

pairwin — 兩下 ⌥Z,把迷失的視窗拉回眼前

一個使用者口述的多視窗痛點,在同一場約 8 小時的 Claude Code session 裡,走完「構想 → 撞死路 → 繞路 → 對抗式審查 → 聖杯」的完整旅程。這是那趟旅程的完整記錄。

10:14 → 18:36 台灣時間 pairwin.lua · 700 行 ⌥Z ×2 完成拉攏
往下看完整故事
亮點

一句話痛點,走了多久才變成「聖杯」?

8 小時 21 分

2026-08-12 10:14 → 18:36(台灣時間),單一 Claude Code session。從「我常常⋯迷失第一個 app 在哪」的口述,走到使用者親口說出「一起拿到聖杯,很開心」。

出處:session 逐字稿首末 timestamp 實測(00be7b8c-...jsonl,UTC 02:14:56 → 10:36:22)

背景與目標

找到了第二個 app,卻弄丟了第一個

使用者一開場就把痛點講得很具體——不是不會切換視窗,是「切過去之後,起點在哪裡」的空間記憶問題。

我在mac多視窗切患有個煩惱 我常常想跨windows把兩個app拉在一起 當我找到第一個我想要的app 然後切換到第二個app的時候 我就會找不到(迷失)第一個app在哪一個window 讓我效率很滴下

使用者原話 · 2026-08-12 10:14

使用者最初的構想是「畫面遮罩高亮顯示」,並主動先問一句「我想先確認你懂不懂我的需求?」——這場 session 從一開始就是雙向確認,不是單向下指令。

先問清楚,才動手

已經解決的,和沒解決的

使用者同一則訊息附上了背景,兩件事都已經做好:

⌨ 快速切換鍵

常用 app 都已設定快捷鍵,可以快速切換焦點。

🗔 視窗位置調整

已有快速視窗位置調整軟體,並設定好快捷鍵。

但這兩者都只在「同一個桌面(Space)」裡有效。缺口精準落在同一件事上:跨桌面把兩個視窗湊在一起時,人一移動、視覺參照就斷了,起點瞬間消失。

先撞死路,才找到活路

macOS 26 封殺的三道路

方案定案之前,Claude 先逐項實測「跨桌面搬視窗」的每一條看起來可行的程式路徑——結果三條全部封死。

hs.spaces.moveWindowToSpace

回報 true(成功),但視窗完全不動——silent no-op,不能依賴回傳值。

模擬滑鼠拖曳視窗

用兩種手法重測(含正確的 mouseDown→連續拖曳→mouseUp),WindowServer 完全拒收,視窗紋風不動。

hs.window.get() 跨桌面查詢

只看得到「目前」桌面(含目前桌面 app 的最小化視窗),別的桌面一律回傳 nil。

出處:本機 2026-08-12,macOS 26.5.2 / Hammerspoon 1.1.1 逐項實測;記憶檔 macos26-spaces-api-dead.md

轉向

不搬視窗,改成「標記+彈跳+就地重開」

三條死路撞完之後,方向反過來:既然舊視窗搬不動,就先標記它、彈過去確認、能重開內容的就在眼前直接開一份新的。

解決的問題:想把兩個不同桌面(Space)的視窗湊在一起對照, 切到第二個app就迷失第一個在哪。

pairwin.lua 檔頭註解

這一步定了整支工具的骨架:兩個標記 🅰🅱、一個熱鍵 ⌥Z 貫穿標記與拉攏、一個 ⌥⇧Z 隨時清除重來。

流程與邏輯

⌥Z 是同一把鑰匙,開三道不同的鎖

整支 pairwin.lua 只掛兩個熱鍵:⌥Z 一鍵包辦「標記→拉攏→重試」,⌥⇧Z 隨時清除。狀態機只有四個節點——快樂路徑走一圈就完成拉攏並自動清空。

尚未標記 marks.A = nil marks.B = nil 🅰 已標記 30 秒倒數(MARK_TIMEOUT) 站在第一個視窗按下 🅱 標記+立即整合 integrateNow() 自動執行 切到另一視窗按下 搬窗失敗 退回即時鏡像 · 唯讀 標記保留,可重試 按 ⌥Z(第 1 下) ⌥⇧Z / 逾時 30 秒 按 ⌥Z(第 2 下) 成功 → doClear() 自動清除 ⌥Z 重試 ⌥⇧Z 隨時可在此放棄,回到「尚未標記」
圖例:實線=往前走一步;虛線=清除或重試;金色=成功後自動 doClear()
品質把關

先讓 15 個懷疑者輪流找碴

方案剛定型,Claude 沒有直接交卷,而是先發動一個對抗式審查 workflow:三個視角並行檢查程式碼,每一項發現都要再被「懷疑者」驗證過一次才算數。

15
位 agent 並行審查
9
項真實缺陷被確認
0
項被懷疑者推翻

Finder 重開順序寫反major

activate 放在 make new 之前,新視窗開錯桌面,使用者還會被硬拉走。

重入競態major

重開流程還沒完成前連按 ⌥Z,會再開一個孤兒視窗,前一輪結果被靜默丟棄。

清除後幽靈標記major

⌥⇧Z 清除時沒取消未完成的計時器,非同步回呼事後把標記寫回來,變成看不見的幽靈配對。

死標記誤刪活配對minor

視窗最小化+app 不在當前桌面時,會被誤判成「已關閉」而清掉還活著的標記。

出處:review-pairwin workflow(agentCount=15,logic/lua 三視角交叉審查+懷疑者驗證,2026-08-12 10:44–10:56);9 項全數修復並裝機驗證

使用者實測回報

「有時候可以,有時候不行」

我試著用⌥Z第一次鎖定 Claude 時沒問題,第二次鎖定 Codex 時沒問題,但我按第三次⌥Z 並沒有把這兩個視窗拉在同一個 Window 裡面 但另外一個情況,像我第二張貼圖,配合的是 Office Word 和 Safari 瀏覽器,它們就被拉到同一個Window裡面 幫我排解測試一下,為什麼有時候可以,有時候不行

使用者原話 · 12:36

使用者同時附上兩張截圖佐證,並在同一則訊息裡主動把權限交出來:「我授權你螢幕操作等最高權限,全部授權給你,請幫我嘗試把功能完整」。

排解

不是隨機故障,是兩種 app 的落差

Claude 排查後指出:這不是隨機的,是設計上早就存在的邊界,只是使用者剛好踩到了。

可就地重開
Word + Safari ✓
  • Finder/Chrome/Safari 都能開一份「內容相同」的新視窗
  • 就地開新視窗+左右併排,最快、人不用被拖走
開不了第二個視窗
Claude + Codex ✕
  • Claude/ChatGPT/Word 這類 app 開不了「第二份相同內容」
  • 當時的方案只能退到「跳過去」,做不到「拉到眼前」

要讓兩種 app 都做到「拉到眼前併排」,唯一的路是把「已經判死刑」的真搬窗救回來。

授權升級

「我授權你螢幕操作等最高權限」

我授權你螢幕操作等最高權限,全部授權給你,請幫我嘗試把功能完整

使用者原話 · 12:36

你再試試,剛剛系統有跳出來跟我要 Hammerspoon 的授權,我剛剛授權了。

使用者原話 · 12:58

這把權限表面上解鎖的是螢幕錄製(給即時鏡像用),但真正打開的是重新挑戰「已經判死刑」的模擬拖曳的機會。過程中 Claude 用 screencapture 自己截圖(mc.png/state.png)驗證畫面狀態,不是憑感覺猜測。

大翻案

死路的隔壁,藏著一條活路

桌面上直接拖視窗確實被 macOS 26 封死(合成游標會動、就是抓不住窗), 但 Mission Control 裡拖縮圖這條通道還活著。

Claude 回報原話 · 13:17

用更正確的手法重測合成拖曳:桌面上直接拖依舊死透;但在 Mission Control 裡拖曳縮圖到空間列——這條 WindowServer 通道還活著。程式讓自己開 MC、用輔助功能讀出縮圖與「桌面 1~11」按鈕的精準座標,合成拖曳把縮圖丟過去。中途踩到一個坑:拖曳懸停時空間列會展開、座標整個位移,解法是「先懸停展開、抄下座標、再拖放」。

真搬窗怎麼做到的

mcMoveHere():5 步、約 5 秒、勿動滑鼠

整個真搬窗是一段阻塞式流程,天然把 Hammerspoon 鎖住,期間使用者的實體滑鼠會跟合成拖曳打架,所以介面上會先跳出「請先別動滑鼠」的警告——這是全程靜默設計裡唯一保留的非錯誤提示。

1
切到目標桌面

gotoSpace(),動畫約 1.2 秒

2
開啟 Mission Control

toggleMissionControl()

3
AX 讀 Dock

抓縮圖與「桌面N」按鈕座標

4
先展開再拖放

懸停展開空間列、抄下位移後座標,合成拖曳縮圖過去

5
放下、切回

收合 MC,切回原桌面——併排完成

出處:pairwin.lua mcMoveHere()(L466-538);空間列按鈕用「桌面N」文字辨識,系統改英文語系需同步改 pattern

聖杯時刻
「一起拿到聖杯,很開心」
使用者原話 · 13:33

真搬窗跑通、全流程實測 PASS 之後,使用者提出最後一個要求:把「標記→確認→拉攏」精簡成「按兩下 ⌥Z」,並且成功後自動清除,不用再按 ⌥⇧Z。Claude 收尾時回了一句:「一起拿到聖杯,與有榮焉。」

精簡後的全貌

拉攏的三層優先序

按下第 2 下 ⌥Z 的那一刻,程式自動挑最快的路——使用者只管按鍵,不用管眼前這兩個視窗是什麼類型。往下捲動,看程式怎麼自動選路。

第一層 · 同桌面
01
同一個桌面 → 直接左右併排

🅰 本來就在目前桌面,getLive() 直接抓到物件,tilePair() 瞬間左右對半分。這是最常見、也最快的情境。

耗時:瞬間 · 函式:tilePair()
第二層 · 可就地重開
02
跨桌面、內容可重開 → 眼前開新視窗

🅰 在別的桌面,但是 Finder/Chrome/Safari——標記當下已經先用 captureContent() 抓好路徑或網址,直接在目前桌面開一份內容相同的新視窗,人完全不用被拖走。

耗時:約 1 秒 · 函式:ensureLocal() + REOPEN_SCRIPTS
第三層 · 真搬窗
03
跨桌面、開不了第二視窗 → Mission Control 真搬窗

Claude/ChatGPT/Word 這類 app 開不了第二份相同內容,程式改走 mcMoveHere():切桌面、開 MC、拖縮圖、放下、切回——把視窗本尊搬到眼前。失敗才退回即時鏡像。

耗時:約 5 秒(請勿動滑鼠) · 函式:mcMoveHere()
關鍵數字

一場 session 留下的工程足跡

逐筆掃過整份逐字稿裡 Claude 呼叫工具的紀錄,看得出這不是「一次到位」,是反覆讀、改、測出來的。

Bash(測試指令) 79 在使用者看到之前,先反覆測試 79 次 Edit(改程式碼) 53 Read(讀檔核對) 11 Write(新檔) 5
出處:掃描 session 逐字稿 assistant 訊息的 tool_use 逐筆計數(2026-08-12 全 session,Bash / Edit / Read / Write 四類)
30 秒
MARK_TIMEOUT · 標記後閒置自動清除
2
個熱鍵貫穿全部功能:⌥Z / ⌥⇧Z
9
項對抗式審查缺陷,全數修復
二次精簡

「只有錯誤,才准跳出來吵你」

我覺得做得不錯。現在可以把所有 Hammerspoon 操作中的提示拿掉(例如跑到哪個進度了之類的),只有在有錯誤訊息的時候才跑出來。 另外也可以試試看另一個方式:如果隔了 30 秒還沒有等到我下一個指令,就直接預設是 Option + Shift + Z,把現在這個動作清掉。這個超時時間(Timeout)我設定為 30 秒。

使用者原話 · 17:45
之前

每一步都彈提示:標記了、切桌面了、開新視窗了⋯畫面一直被打斷。

之後

全程靜默,只有 ⚠️ 錯誤才彈;進度只看右下角常駐 HUD;標記閒置 30 秒自動清除。

使用者追問

「視窗最後會被抓去哪裡?」

因為我現在用起來發現,好像我執行第一次,它會在某一個視窗做左右並排;執行第二次,它好像也會在同一個視窗做左右並排。也就是說,那個視窗執行越久之後,會有越多的 App 疊在上面。

使用者原話 · 17:45

歸屬規則:拉攏永遠落在「按第 2 下 ⌥Z 時,人所在的桌面」——🅱=錨點不動,🅰=被搬過來。這是設計行為,不是 bug:代價是常按第 2 下的那個桌面會越堆越多視窗,只進不出。

經過你的解說,我也理解了,那目前就先不用改,我知道以後該怎麼處理了,現在的機制反而是最好的。

使用者原話 · 17:48
程式碼瘦身

789 行 → 700 行

互動層面「兩下拉攏」已經是理論下限,省不了;但程式碼層面把兩下拉攏模式下永遠跑不到的舊三段式流程整段刪除,迴歸測試全綠(併排 ✅ 自動清除 ✅ 倒數啟停 ✅)。

700 行(現檔)
700 行留下的功能程式碼(88.7%)
−89 行:舊三段式流程死路徑(11.3%)
← 砍掉的是「彈跳/召喚整組/鏡像切換」那套已無路徑可達的舊邏輯,不是功能
番外

請 Claude 順便幫忙精簡「自己」的提示詞

使用者把自己剛才那段落落長的需求貼回來,請 Claude 重寫一次,順便問問「以後該怎麼講才更直接」。

重寫前
423 字

8 句話、含「因為」「另外」「這個對話分兩個部分」等口語迂迴敘述

重寫後
109 字

4 行、動詞開頭、要做的和只是問的分開標示,資訊零損失

字元數為逐字稿原文實測(不含空白換行),減少約 74%;Claude 當下回應原話估算為「從 300 字到 80 字」,方向一致、量級略有出入

動詞開頭、一行一件事 要做的/只是問的分開標示 問規則,不用猜選項 觀察現象當證據,一句就好

你原文的「我覺得做得不錯」其實不算廢話(明確告訴我方向對了、不用回頭重審), 真正的冗餘只有重複的數字和三連猜。你的提示詞底子比你自己評的好。

Claude 回應原話 · 18:09
踩雷總覽

8 小時裡的關鍵轉折

10:14
使用者提出痛點,Claude 先確認需求再動手
10:39
撞死路 ×3(moveWindowToSpace / 拖曳 / window.get 跨桌面),轉向「標記+彈跳+就地重開」
10:44
發動 15-agent 對抗式審查,確認 9 項真實缺陷並修復
12:36
使用者回報「有時候可以,有時候不行」,並授權螢幕操作最高權限
13:17
大翻案:Mission Control 拖曳縮圖復活,真搬窗成功
13:33
使用者:「一起拿到聖杯,很開心
13:52
最終形態上線:兩下 ⌥Z 拉攏+自動清除
17:45
使用者要求全面靜默化+30 秒逾時,並追問視窗歸屬邏輯
18:08
程式碼瘦身 789 → 700 行,迴歸測試全綠
18:13
番外:Claude 幫使用者把自己的提示詞精簡(423→109 字)
現在的樣子

掛在 init.lua 裡的一行 require

477-- 視窗配對彈跳(⌥Z 標記→配對→整合一鍵搞定;⌥⇧Z 清除):詳見 ~/.hammerspoon/pairwin.lua 478require("pairwin")
熱鍵作用
⌥Z標記 🅰 → 標記 🅱+立即拉攏 → 殘局重試,同一鍵包三個階段
⌥⇧Z隨時清除配對,靜默不彈提示
pairwin.lua · 700 行 · 27,848 bytes pairwin_marks.json · 存開機時間戳防重用認錯人

備註資料夾對照

  • 工具本體在 ~/.hammerspoon/pairwin.lua;本資料夾(~/Scripts/視窗拉攏/)僅存這份簡報。
結論與下一步

從一句話到聖杯,然後呢

8 小時 21 分,一個使用者的空間記憶痛點,變成一支 700 行、兩個熱鍵、能對三種 app 類型自動選路的 Hammerspoon 工具。過程經過 15-agent 對抗式審查、使用者兩輪實測回報、一次技術大翻案,最終使用者自己驗收「聖杯」。

下一步如果要往前走:目前工具沒有留下任何使用紀錄,量化「這把工具到底多常被用、多常失敗」需要先補一層最輕量的紀錄(例如失敗時的 alert 順手記一行 log),而不是繼續憑感覺判斷好不好用。

待補誠實揭露:現在還不知道的事

  • 上線後實際使用次數/成功率:目前無 telemetry,pairwin_marks.json 只存最後一次配對狀態,不是使用計數
  • 長期穩定性(不同 app 組合、不同天數):目前只有 session 內的單次驗收記錄,不是長期統計