Repository navigation
2026-06-18
- Claude Code sidebar now shows model name, no Python dependency —
fetchClaudeAgentsDatarunsclaude agents --jsondirectly via/usr/bin/env, parses JSON withhs.json.decode, and reads the session's.jsonlfile in pure Lua to extractmodelID. RemovedRESOLVE_PYTHON_PATH(),buildCCModelCmd(), and thepythonPathconfig option. The entire enrichment pipeline is now implemented in Lua usingio.openandhs.json.decode. Displayed as aDETAIL_COLORrow under the "claude" header. Also fixed astring.formaterror caused by an unescaped%in the old Python inline script. - Claude CLI path auto-resolution — replaces the broken
which claudeasync check withRESOLVE_CLAUDE_PATH()that probes~/.local/bin/claude,/opt/homebrew/bin/claude, and/usr/local/bin/claudeat startup. The claude binary is invoked via/bin/sh -cwith the full resolved path. - Parsed-agents log gated behind
debugconfig,SHORT_MODEL_NAMEstripsclaude-prefix.
2026-06-16
-
Removed tiling on/off feature (
_tilingEnabled,toggleTiling(),⌘⇧Thotkey) — hiding the sidebar already disables tiling, making a separate toggle misleading. Tiling is now always active when the sidebar is visible. The tiling cascade guard (_tilingInProgress,_withTilingGuard) is preserved as a stability mechanism. Removed:_tilingEnabledstate var and all 4 guard conditions acrosswindows.lua/render.lua/watchers.lua,toggleTiling()function,toggleTilingfromACTION_LABELSandbindHotkeys(), and the Phase 3 "sidebar follows focus when tiling off" block inwindowFocusedhandler. -
Cleaned up
_sidebarEnableddead code — this variable was initialized totrueand never mutated. Removed frominit(),syncCanvasLevel(),refreshSleepWake(),_setupSpaceWatcher(), and 5 async callback guards infetchers.lua. -
Fixed sidebar-hidden dead-end state where
_sidebarVisiblecould only be recovered via explicit toggle —_doBuildSidebar()was setting_sidebarVisible = falsein two non-user-intent paths: (1) the#wins == 0branch (no iTerm windows), and (2) thepcallerror handler. Once_sidebarVisiblewasfalse, the earlyif not self._sidebarVisible then return endguard prevented ALL future rebuilds through watchers/refresh — onlytoggleSidebar()could recover it. Fixed by removing both_sidebarVisible = falseassignments — the system now only hides/deletes the canvas without overwriting user intent.toggleSidebar()no longer callstileITermWindows()on hide (canvas just hides, windows keep their positions). Addedpcallerror capture (local ok, err = pcall(...)) withprint()in the error handler so the root cause of any "sidebar disappears on refresh" crash can be diagnosed. -
macOS native full-screen (green button) now hides sidebar and excludes full-screened windows from tiling — Space watcher calls
_checkFullScreenAndAdjust()on every Space switch; whenhs.spaces.spaceType()returns"fullscreen", the sidebar canvas is hidden (removed from full-screen Spaces for other apps and full-screened iTerm windows)._doBuildSidebar()also checks_isCurrentSpaceFullScreen()as a guard on every rebuild. Full-screened iTerm windows are filtered fromgetITermWindows()viaw:isFullScreen(), keeping them out of the tiling pool. Option-click (Zoom/fill) is unaffected —isFullScreen()returnsfalseand no Space change occurs. -
Added
screensDidWake/screensDidUnlockto sleep watcher —refreshSleepWake()now also fires when the display wakes or the user unlocks the computer (not just system sleep wake), re-creating event taps and rebuilding the sidebar to fix unresponsive clicks after sleep/lock.
2026-06-15
- Fixed sidebar not showing on build and eliminated double-resize — reordered
_doBuildSidebarso that_sidebarVisible = trueandtileITermWindows(sb)run beforesyncCanvasLevel()andshow(). Previously windows were tiled after the canvas appeared, causing: (a) two visual transitions (canvas shows, then windows resize); (b)setFramereshuffling z-order and burying the just-shown canvas. Now windows resize first while the canvas is still hidden, then the canvas appears already at the correct level above the tiled windows — single visual transition.
2026-06-12
- Tiling cascade guard — added
_tilingInProgressflag set before anysetFrameloop (via_withTilingGuard) and cleared aftersettleDelay + 0.1s.handleWindowMoveOrResizechecks and bails early if a tile is in flight, breaking thesetFrame→windowMoved→ retile cascade. Also guards the drift-pathsetFrameloop inhandleWindowMoveOrResize. Cleaned up instop()/init(). - Eliminated double-tile on sidebar show — show path in
toggleSidebarnow inlines anchor computation and callsbuildSidebar()directly (no synchronoustileITermWindows), letting_doBuildSidebarbe the sole tile trigger.refreshLayoutsets_skipTileOnThisBuild = trueafter its sync tile to prevent_doBuildSidebarfrom tiling again. - Guard watchers during hide —
_rebuildAfterSettlenow checks_toggleLockbefore scheduling any rebuild, preventing stale work during the 0.5s hide/show lock window. - Reload hygiene —
stop()now cleans up_resizeDebounceTimer,_tilingClearTimer,_tilingInProgress,_pendingSidebarFrame, and_currentScreen.init()initializes_tilingInProgressand_tilingClearTimer.
2026-06-09
- Phase 1: Decouple tiling from sidebar — added
_tilingEnabledstate variable (init'dtrue),toggleTiling()function, and⌘⇧Thotkey. No behavioral change yet — the toggle flips state and re-tiles when re-enabled, but nothing is gated behind it (Phase 2 adds the actual guards). - Phase 2: Gate all window frame manipulation behind
_tilingEnabled—tileITermWindows(), thesetFrame()loop inhandleWindowMoveOrResize(), the retile guard in_doBuildSidebar(), and_rebuildAfterSettle()all check_tilingEnabledbefore touching any window frame. Toggling tiling OFF now actually prevents window layout changes; the sidebar repositions itself to the focused window but does not resize/move any iTerm windows. - Phase 3: Sidebar follows focused window when tiling is off —
windowFocusedhandler repositions the sidebar canvas to the newly focused iTerm window using directcanvas:setFrame()instead of going through thebuildSidebar()pipeline. This bypasses the snapshot-check/debounce/rebuild machinery and moves the canvas instantly. Only fires when position actually differs (avoids redundant calls on stacked windows).
2026-06-06
- Fixed canvas unresponsive after sleep/wake — added
hs.caffeinate.watcher(_setupSleepWatcher) that re-creates the click and drag event taps and rebuilds the sidebar canvas on wake from sleep, sincehs.eventtapinstances can be silently invalidated by the system after macOS sleep. AddedrefreshSleepWake()helper that invalidates all state, re-creates event taps, and conditionally rebuilds if the sidebar was visible. - Attempted to fix sidebar toggle reliability —
_sidebarVisibleis now the single source of truth intoggleSidebar(decoupled fromsidebarCanvasexistence). Screen watcher (_setupScreenWatcher) no longer forces_sidebarVisible = trueand only rebuilds if the sidebar was already visible. Space watcher (_setupSpaceWatcher) now checks_sidebarVisiblebefore rebuilding. Added_toggleLockto the show path for consistency with the hide path. Added_sidebarVisibleguards to all async fetcher callbacks to prevent unnecessary rebuilds when the sidebar is hidden. - Attempted to fix tiling never firing after first build —
_doBuildSidebarbody is now wrapped inpcallso_buildPendingis always reset (preventing the "stuck" state where any Lua error silently blocks all future rebuilds and tiling). AddedtileITermWindows()call directly inrefreshLayoutso tiling always fires on explicit layout commands regardless of_doBuildSidebar's snapshot-based early-exit logic.
2026-06-05
-
Fixed sidebar context loss on focused Claude Code window during spinner animation — when the focused iTerm2 window had a Claude Code session running, every spinner tick in the title (
⠋ /path,⠙ /path, etc.) triggeredwindowTitleChanged. ThestableCore()comparison was intended to detect these as no-ops, butlastRawTitlewas never seeded onwindowFocused— it was only written bywindowTitleChangeditself. So the very first spinner frame after focusing always diffed against a nil/stale baseline,newCore != prevCore, which invalidatedwd/tabInfoand firedFETCH_WINDOW_INFOwith the spinning title. The AppleScriptfirst window whose name = "⠋ /path"failed to match, leavingtabInfoasMISSINGand the sidebar falling back toparts.basename(the last word of the title). Fixed by seedingCACHE.wc(winId).lastRawTitle = win:title()in thewindowFocusedhandler so the first spinner diff always has a clean baseline. The bug only manifested on the focused window because unfocused windows don't receivewindowFocusedand theirlastRawTitlewas already populated from prior title changes. -
Attempts to fix sidebar cache corruption during Claude Code spinner animation — CC's dynamic spinner frames (braille U+2800-U+28FF) in the window title lacked the
✳/·/🔔prefix, sowindowTitleChangedtreated them as real navigation events, callingCACHE.invalidateWindowwhich wipedwd/tabInfobefore the async AppleScript could re-populate them. The AppleScript then failed because the spinner-frame title didn't match any iTerm window name. Replaced the fragile CC-prefix-based invalidation gate with astableCore()comparison — strips all stable CC decorations (✳,·,🔔, braille U+2800-U+28FF, trailing resolution suffix) and only invalidates when the meaningful path portion actually changed.- None of this actually helped
-
Attempts to fix sidebar button flickering during Claude Code thinking — Claude resets the terminal title to its base form between prefix transitions (
✳ /path→/path→· /path). This triggered two bugs inwindowTitleChanged:- The intermediate
/pathlacks a CC prefix, so the handler treated it as a real navigation, callingFLASH.stopFlashing(resetting button color to blue) and scheduling a stale rebuild — despite the cache being intact. Fixed by storinglastRawTitleper-window andreturn-ing early when the stripped previous title matches the current non-CC title, skipping ALL flash changes and rebuilds for transient resets. - The
·(busy) state fell through to theelsecatch-all which calledstopFlashingwith the normal (blue) color instead of busyColor (green) from agents data. Added an explicitelseif state == "busy"that stops flashing but schedules a rebuild so_gatherWindowDatarenders the correctbusyColor.
Also renamed_fetchWindowInfo→FETCH_WINDOW_INFO(UPPERCASE convention), changed guard totype(tabInfo) == "table", and preserved all cached fields in the AppleScript callback when the AS fails mid-flight.
- None of this actually helped
- The intermediate
-
Code review cleanup (Phase E): Made
_adjustFlashTimerlocalinflash.lua. RenamedshouldShowAndTile→needsRetilewith clarifying comment about pre-render timing. GuardedCACHE._claudeAgentsDataagainst wipe on empty poll results. AddedCACHE.MISSINGsentinel constant replacing barefalsein 7 fetcher assignments. Hoistedhs.window.focusedWindow()out of per-window loop in_gatherWindowData. Scoped all 6 flash module variables (_sharedFlashTimer,_currentFlashInterval,_flashingWindows,_flashState,_flashNormalColor,_flashType) tolocalwithFLASH.flashState(winId)getter andFLASH.reset()replacing the duplicate reset blocks ininit()/stop(). -
CR follow-up: Added
:stop()before nilling_sharedFlashTimerinFLASH.reset()to prevent timer leak. Added sentinel comment forCACHE.MISSING(nil= not yet fetched,false= fetched but absent). -
Fixed canvas disappearing on async data updates —
_renderFullSidebaralways runs when the sidebar snapshot changes (triggered by async callbacks: git branch, claude/opencode polling, window titles, etc.), deleting the old canvas and creating a new hidden one. Butshow()was gated byshouldShowAndTile, which only fires when the window structure (count/dimensions) changes. Any non-structural snapshot change caused the canvas to hide. Movedshow()and_sidebarVisible = trueoutside the guard — only tiling remains conditional. Also removed_flashStatefromsidebarStateSnapshotsince the flash timer handles its own visual updates via directelementAttributemutations, reducing unnecessary rebuilds. -
Global naming convention cleanup — moved display-state globals to
RENDER.*namespace (sidebarStateSnapshot,startFlashing,claudeState, 14 total), uppercased stateless helpers (COLOR,IS_ITERM,GET_GIT_BRANCH_FOR_PATH,BASE_PATH, 12 total), left_-prefixed internals as-is..luarc.jsondiagnostics.globalsshrunk from 8 entries to just["hs"]. Zerolowercase-globalwarnings. -
Code review cleanup (Phase A): Fixed
closeMenu()in_renderPopupMenu— replaced manual teardown withself:_closeMenus()call, removing 8 lines of duplicate code. Removed duplicate_currentFlashInterval = nilin_adjustFlashTimer. -
Code review cleanup (Phase B): Created
FLASHnamespace for flash functions (claudeState,startFlashing,stopFlashing, etc.), moved offRENDER.isSidebarClickAllowedchanged fromRENDER.*global tolocalinmouse.lua. Converted_syncOrderedIdsfrom free function toOBJ:_syncOrderedIds(). MadeHEADER_COLOR/DETAIL_COLOR/PAD_TOP/GAPlocalinrender.lua. Namespaced_claudeAgentsDataunderCACHE._claudeAgentsData. -
Code review cleanup (Phase C): Moved 5 hardcoded color literals to config (
borderColor,wsNameColor,branchColor,menuBgColor,menuHighlightColor). -
Code review cleanup (Phase D): Moved
OBJ:findWindowScreenandOBJ:getScreenfromcache.luatowindows.lua(screen resolution is layout logic, not cache logic). -
Fixed opencode data fetch hanging: sqlite3 fallback query on large DBs (~121MB) could block indefinitely, leaving
_opencodePendingstuck attrueand preventing all future polls. AddedLIMIT 50to the query and clear_opencodePendingbefore launching the sqlite3 task so subsequent polls aren't blocked. -
Phase 1 (Change 1): Fixed
_hotkeyLabelsdead state — addedformatHotkeyLabel()helper withMOD_SYMBOLS/KEY_SYMBOLSlookup tables that convert modifier/key arrays to display strings (e.g.{"cmd","shift"},"S"→"⌘⇧S").bindHotkeys()now populatesself._hotkeyLabelsfrom the binding parameters, so user-customized hotkeys automatically update context menu shortcuts. Removed the deadself._hotkeyLabels and self._hotkeyLabels.xxx or "fallback"pattern fromshowWindowMenu/showGlobalMenu. -
Phase 1 (Change 2): Replaced inline menu teardown with
self:_closeMenus() -
Phase 1 (Change 3): Fixed
_fetchWindowInfosilent false-negative -
Phase 1 (Change 4): Added
busyColorto config -
Phase 1 (Change 5): Fixed
startFlashingmixed-type interval with_adjustFlashTimer()— extractedflashIntervalForType(),minActiveFlashInterval(), and_adjustFlashTimer()helpers.startFlashingandstopFlashingnow both call_adjustFlashTimer(), which (re)creates the shared timer at the minimum interval across all active flash types. When a fast-type window leaves, the timer correctly grows back to the next shortest interval. Removed the single-type interval assignment and manual teardown instopFlashing. — replaced the only hardcoded inline color in_gatherWindowDatawith a newcfg.busyColorentry, making it themable like all other colors. — early-return condition~= nilmatched bothnil(unfetched) andfalse(fetch returned empty), preventing retry when AppleScript failed. Changed to truthy check sofalsevalues trigger a new fetch while respecting the pending guard. —showWindowMenuandshowGlobalMenueach had an identical 8-line inline block duplicating_closeMenus(). Replaced both with a singleself:_closeMenus()call. -
Phase 2 (Change 1): Extracted
_syncOrderedIds()— the ~20-line live-window reconciliation block (filter_orderedWindowIdsagainst current windows, append new ones) was duplicated inmoveWindowById,moveWindowToExtent, andfocusNextWindow. Extracted into_syncOrderedIds()helper, which also handles_orderedWindowIdsinitialization. The slightly different pattern infocusNextWindow'selsebranch and its empty-IDs fill branch both reduce to the same_syncOrderedIds()call. -
Phase 2 (Change 2): Extracted
normalizeOCSession()— thes.modelJSON decode + record construction was duplicated in both the HTTP and SQLite branches offetchOpenCodeData. Extracted intonormalizeOCSession(s)pure helper. Callers keep their different dedup strategies (HTTP: replace-when-newer, SQLite: first-seen-wins). -
Phase 2 (Change 3): Extracted
windowStatusColor()— the 7-branchbtnColorchain in_gatherWindowDataextracted into a pure function taking(state, isActive, isDragHover, isFlashing, isFocused). Capturescfgas upvalue. Replaced inline chain and removed the orphanedlocal btnColordeclaration. -
Phase 2 (Change 4): Renamed
needsFullRebuildtoshouldShowAndTile— in_doBuildSidebar, the variable controls whether to show the canvas and tile (rendering is always full via_renderFullSidebar). New name matches actual semantics. -
Phase 3: Collapsed 10 parallel cache tables into
_winCache— replaced_gitBranchCache,_gitBranchPending,_gitWsNameCache,_prCache,_prBranchCache,_prPending,_wdCache,_tabInfoCache,_tabInfoPending,_hostnameCachewith single_winCache[winId]record containing.wd,.tabInfo,.tabPending,.hostname,.branch,.brPending,.wsName,.pr,.prBranch,.prPendingfields. Addedwc(winId)auto-vivify helper andinvalidateWindow(id, fields)for partial clears. All 10 invalidation sites replaced with_winCache[id] = nil(full) orinvalidateWindow(id, {fields...})(partial). Full resets ininit()/stop()collapsed from 10 lines to_winCache = {}. -
Phase 4: Decomposed single 2318-line init.lua into 8 focused modules — extracted
cache.lua,flash.lua,fetchers.lua,render.lua,windows.lua,mouse.lua,watchers.lua. Usesdofilewith global declarations (nolocalon shared symbols).init.luareduced from 2318 to 406 lines. -
Phase 4:
CACHEnamespace for cache globals —_winCache→CACHE.winCache,_iTermWindowsCache→CACHE.iTermWindowsCache,getITermWindows()andwc()moved toCACHEtable. Intentional bare globals (isITerm,color, etc.) listed in.luarc.jsonto silence lua-language-serverlowercase-globaldiagnostics.
2026-06-04
-
Phase 4: Tab-name buttons with
・dot indicators: label now shows the iTerm2 tab name (from_tabInfoCache) wrapped in・dots indicating tab count and focused position (e.g.・tabname・).makeTabLabel()helper safely handles missing/falsy tab info, unknown focused index (falls back to position 1), and edge cases. Basename suppression compares against plaintabNameinstead of the dotted label. When tab info is unavailable (async miss), falls back tobasename or hostname or "Window N"with no dots. -
Fixed tmux window support in
_fetchWindowInfo: replaced session-ID matching tab iteration withtitle of current tab+ title-match for focused index — session IDs differ between window and tab contexts for tmux connections, causing the script to return""and block all cache population. Also always sets_tabInfoCache[winId](tofalseon failure) to prevent infinite re-fetch loops. -
Phase 3: Hostname as separate line —
_hostnameCache[winId](fallbackparts.hostfrom title parsing) rendered as line 2 inbuildTextRowswithDETAIL_COLOR, suppressed when identical to label. Label fallback updated to usehostnameinstead ofparts.hostdirectly. Label itself unchanged — hostname line sets up Phase 4 where label becomes tab name and they diverge. -
Phase 2: Combined tab-info + session variable AppleScript — replaced
getWindowWorkingDir()with_fetchWindowInfo(win)that returnstabCount\x1efocusedIdx\x1etabName\x1epath\x1ehostnamein a single call. Added_tabInfoCache,_tabInfoPending,_hostnameCachemodule-level tables. Removed_wdFlight. All callers updated to_fetchWindowInfo(win)+_wdCache[winId]. Cache invalidation added for_tabInfoCache/_hostnameCacheinwindowDestroyed,windowTitleChanged,handleWindowMoveOrResizescreenChanged. No visible UI changes — path/branch display unaffected. -
Phase 1: Removed custom rename feature — stripped all rename/custom-name infrastructure (~200 lines): removed
renameWindow,_saveCustomName,startRenameMode,_updateRenameBar,_handleRenameKey,commitRename,cancelRenameMode,_positionRenameBarmethods; removed_renameMode/_renameBuffer/_renameWindowId/_renameEventTap/_renameBlink/_renameBarIdx/_renameTextIdx/_cursorVisible/_customNames/_customNamesByPath/_pendingPathNamesstate fields; removedSETTINGS_KEY_NAMES_BY_PATHpersistence key; removed rename bar elements from_renderFullSidebar; removed rename cancel guards from 4 watcher/eventtap locations; removed rename hotkey (⌘⇧E) and Rename context menu item; removed path-name persistence instop()/_restorePersistedState()/getWindowWorkingDircallback. Label falls back toparts.host or basename or "Window N". No custom names. -
Rename bar repositioned above target button: instead of always appearing at the bottom of the sidebar, the rename bar now appears just above the button being renamed (2px gap, clamped to not go off-screen).
_renderFullSidebarno longer hardcodessb.h - BAR_H - BAR_BOTTOM_MARGIN; positioning extracted into_positionRenameBar()helper called from bothstartRenameModeand_renderFullSidebar(so the bar stays positioned correctly on async rebuilds during rename mode). Also addedstrokeWidth = 0to the bar rect element to fix a thin black line artifact from the default 1px stroke. -
Canonical hotkey dictionary + alerts: added
formatHotkeyLabel()helper withMOD_SYMBOLS/KEY_SYMBOLSlookup tables that produce the correct display string from modifier/key arrays (e.g.,{ "cmd", "shift" }, "S"→"⌘⇧S").bindHotkeys()now stores all resolved labels inself._hotkeyLabels[name]and fireshs.alert.show()on every hotkey press. All 13 hardcoded UTF-8 shortcut strings inshowWindowMenu()andshowGlobalMenu()replaced withself._hotkeyLabels.*references — changing defaults or user overrides auto-updates everywhere. Added completion alerts for the 3 slow operations:toggleSidebar()shows "Sidebar Hidden"/"Sidebar Shown",toggleSide()shows "Swap Side Complete" after_doBuildSidebarfinishes tiling, and the refresh hotkey shows "Layout Refreshed". -
Single render path: removed
_renderInPlace()and all dual-path machinery (_elementMap,_btnStructureKeys,buttonStructureKey(),bKey,needsAnyWindowRebuild)._doBuildSidebar()now always calls_renderFullSidebar()— thesidebarStateSnapshot()early-exit guard already prevents unnecessary work. This eliminates the class of bug where text element frames desync from click areas when row counts change (e.g., a claudewaitingForline appears). ~80 lines removed. -
Fixed
getWindowWorkingDir(): changed AppleScript to match by window title (whose name = "...") instead of by CGWindowID (whose id is %d). Hammerspoon'swin:id()returns a CGWindowID that does not match iTerm2's AppleScript window ID, causing every path lookup to fail (Can't get current session) and leaving_wdCache[winId] = false. Title-based matching with proper\and"escaping unblocks git branch display, claude section, and opencode path matching. -
Removed Claude Code .jsonl parsing: the never-relaible file-based reading of
~/.claude/projects/*.jsonlfor model/token data has been removed along withclaudeEncodeDir(),claudeProjectDir(),fetchClaudeCodeForWindow(),fetchClaudeCodeData(), and all associated caches/render code. Theclaudecodeconfig key is kept for backward compat. -
Replaced with
claude agents --jsonpolling: newfetchClaudeAgentsData()runsclaude agents --jsonon each poll interval (default 5s), parses structured JSON indexed bycwd, and populates_claudeAgentsData[cwd] = { status, waitingFor }. Matches windows by their working directory (_wdCache). NewstartClaudeAgentsPolling()wired intostart()/stop()/init(). TheclaudeState()title-based heuristic (✳/·/🔔) is kept for flash responsiveness;claude agents --jsonstatus will be preferred for button color in a follow-up. -
State detection upgrade:
_gatherWindowData()now prefersclaude agents --jsonstatus ("waiting"/"busy") over the title-prefix heuristic (✳/·), with🔔bell kept from title only. The heuristic remains as fallback when agent data is absent or reports"idle". -
Dynamic button layout with no-gap text packing: replaced the hardcoded 5-line button layout with dynamic
textRowscomputed per-window viabuildTextRows(). Lines pack without gaps — each line (basename, branch, opencode section, claude section + waitingFor + status) only appears when its data exists. Button height is uniform across all windows, computed ascomputeBtnHeight(maxNumRows, dfs)._elementMapnow stores arowsarray of element indices instead of namedline1..line5fields.buttonStructureKey()simplified to just the row count. RemovedbtnFontSizes()and oldbtnHeight(). NewcomputeTextArea()helper for text positioning within each button. -
_gatherWindowData()now skips the redundantcfgparameter (module-levelcfgis already in scope). -
Configurable font sizes: added
defaultFontSizeconfig option (default11). Line 1 (label) renders atdefaultFontSize + 1, lines 2–3 (basename/branch) atdefaultFontSize, lines 4–5 (opencode/claudecode) atdefaultFontSize - 1. Button height auto-calculates from the font sizes viabtnHeight()/btnFontSizes()helpers.
2026-06-03
-
Extracted
_rebuildAfterSettle()helper: consolidated 4 identicalhs.timer.doAfter(settleDelay, ...)blocks from windowCreated/windowDestroyed/windowMinimized/windowUnminimized watcher callbacks into a single_rebuildAfterSettle(tileWhenHidden)method — reduces duplication, makes the settle-delay pattern explicit -
Tile inside
_doBuildSidebar(): movedtileITermWindows()call into_doBuildSidebar()so tiling fires after the canvas is fully built with correct geometry; added_skipTileOnThisBuildflag to prevent double-tiling fromhandleWindowMoveOrResize(sets flag before callingbuildSidebar, checked inside_doBuildSidebar) -
tileITermWindows()now accepts optional sidebar anchor: passes the freshly-built sidebar frame (sb) directly instead of re-reading it viagetSidebarAnchor(), eliminating a stale-frame race -
toggleSidebarhide usestileITermWindows(): replaced manual frame arithmetic in the hide branch with a direct call totileITermWindows(), consistent with all other tiling paths -
De-monolith
_doBuildSidebar(): extracted 5 pure-block-move helpers (_closeMenus,_orderedWindows,_gatherWindowData,_renderFullSidebar,_renderInPlace)._doBuildSidebar()shrunk from ~477 lines to ~58. No behavior change. -
Stripped redundant
tileITermWindows()calls: removed all 10 externalself:tileITermWindows()calls that were unnecessary duplicates of the tiling triggered bybuildSidebar()→_doBuildSidebar(). Tiling now fires only via the 50ms debounced_doBuildSidebarrather than also from each caller. -
Reverted tile-timing fix: the Phase 2 attempt to move
tileITermWindows()inside_doBuildSidebar()caused a cycle (setFrame→windowMoved→ watcher →handleWindowMoveOrResize→buildSidebar→_doBuildSidebar→setFrame→ ...) and snapped windows to tiled position on drift-triggered rebuilds. The stale-canvas-frame race condition will be addressed separately. -
Refactored
start()into extracted methods: pulled 5 watcher setup blocks into named methods (_setupWindowWatcher,_restartWindowWatcher,_setupAppWatcher,_restorePersistedState,_setupScreenWatcher,_setupSpaceWatcher) —start()reduced from ~260 lines to ~15, each extracted method has a clear single responsibility -
New
settleDelayconfig option (default0.3): replaces 8 previously hardcoded delays across all watcher subscribers (windowCreated, windowDestroyed, windowMinimized, windowUnminimized, windowTitleChanged, handleWindowMoveOrResize, screenWatcher, spaceWatcher) with a single tunable setting -
Right-side sidebar support: added
sidebarSideconfig ("left" or "right"),layoutFrames()replacescomputeLayout()for bidirectional layout math, newtoggleSide()method and⌘⇧Shotkey to swap sides at runtime;handleWindowMoveOrResizedrift detection andtoggleSidebarhide restore both handle right-side positioning -
Start hidden mode: added
startHiddenconfig — whentrue, sidebar starts invisible;_sidebarVisibleinitialized from config instead of alwaystrue -
Drag-over-sidebar tab merging: new
_dragWatchTapeventtap monitorsleftMouseDragged/leftMouseUp— dragging a tab over a sidebar button brings the corresponding iTerm window to front and highlights the button withdragHighlightColor(green). Button color reverts when drag leaves the sidebar or ends -
Combined left/right click eventtap:
_leftClickTap+_rightClickTapmerged into single_clickTapvia_setupSidebarClickTap();handleSidebarClicknow activates iTerm2 app on any sidebar click, focuses the window on left-click, and callsshowGlobalMenu()for right-clicks on empty sidebar area -
Global right-click menu:
showGlobalMenu()appears when right-clicking empty sidebar space — offers Refresh Layout, Show/Hide Axis, Swap Side;_renderPopupMenuextracted fromshowWindowMenufor reuse -
Removed
SETTINGS_KEY_NAMESpersistence:_saveCustomNameno longer writes tohs.settingsunder the old key; custom names are now only stored by path inSETTINGS_KEY_NAMES_BY_PATH -
buildSidebardefers during open menu: sets_needsRebuildflag when menu is visible; rebuild fires after menu closes, preventing stale canvas state -
_sidebarVisibleguards added: to_doBuildSidebar,tileITermWindows, andhandleWindowMoveOrResizefor safety against stale geometry or rebuilds when sidebar is hidden -
_sidebarVisibleset earlier in toggle: moved beforerefreshLayout()call to prevent race with debounced_doBuildSidebar -
_toggleLockintoggleSidebarhide: prevents drift detection during programmatic window moves when hiding the sidebar -
Fixed left-click not registering when non-iTerm app is frontmost: inverted
orderedWindows()guard to block clicks only when a non-iTerm2 window actually covers the click point, rather than requiring an iTerm2 window to be topmost (which fails when another app is frontmost) -
Fixed toggle show/hide still looping: removed duplicate
tileITermWindows()call from show branch oftoggleSidebar—refreshLayout()already callsbuildSidebar→_doBuildSidebar→tileITermWindows, so the second call was creating an extra wave ofwindowMovedevents that fired after_toggleLockexpired -
Fixed
f:contains(clickPt)nil error: replacedhs.geometry.rect:contains()call with a manual bounds check, sincew:frame()can return a plain Lua table (truthy but lacking the:contains()method) for off-screen/minimized windows -
Applied
styluaformatting pass across the entire file for consistent indentation -
Fixed sidebar left clicks being swallowed: corrected
rectContains(f, ...)→rectContains(sf, ...)variable name bug in_leftClickTap—fwas nil at that scope, causing the sidebar click handler to always skip -
Cleaned up
syncCanvasLevel: removed dead code (window-level scanning loop, unusedtargetWin/best/bestLvlvariables) that was calculating the topmost iTerm window level but never using the result -
Changed
syncCanvasLevelnon-iTerm fallback fromfloating - 1tonormalto prevent sidebar from floating above other apps -
Consolidated canvas level setting: removed duplicate
_appWatcherlevel calls (floatingon activate,floating - 1on deactivate) sosyncCanvasLevelis the single source of truth, eliminating race conditions between the two systems -
Replaced manual bounds check in
_leftClickTapwindow-scanning loop withrectContainshelper for consistency -
Simplified
syncCanvasLevel: always usesnormalwindow level +orderAbove(nil)instead of toggling betweenfloatingandnormal; other apps naturally layer above the canvas when frontmost, eliminating level-based race conditions -
Replaced
hs.window.orderedWindows()walk in_leftClickTapwithisSidebarClickAllowed()helper (checks frontmost app is iTerm2 or Hammerspoon) for simpler, more reliable click gating -
Changed eventtap handlers to return
falseinstead oftrue— sidebar clicks are no longer swallowed at the OS level, fixing focus forwarding and click-through issues -
Replaced
canvasMouseEvents(false, false, false, false)withclickActivating(false)+ noopmouseCallbackin_doBuildSidebarto prevent canvas from intercepting clicks while still allowing eventtaps to work -
Removed
debugTitles()debugging utility no longer needed in production -
Added
.vscode/*to.gitignore -
Fixed toggle sidebar loop: added
_toggleLockflag that suppresses drift detection inhandleWindowMoveOrResizeduring programmatic window moves fromtoggleSidebar; also added_sidebarEnabledguard totileITermWindowsto prevent stale geometry tiling when the sidebar is hidden -
Fixed
_leftClickTap: restoredhs.window.orderedWords()walk to determine the topmost window at the click coordinate — thefrontmostApplication()shortcut dropped all sidebar clicks when any other app had focus, even if iTerm2 windows covered the sidebar area -
Fixed
_appWatcherdeactivation level: changed fromwindowLevels.normaltowindowLevels.floating - 1for consistency withsyncCanvasLevel(normal level gets buried under other app windows on recent macOS) -
Fixed orphaned context menu taps:
_doBuildSidebarnow cleans up_menuCanvas/_menuEventTap/_menuKeyTapbefore any canvas rebuild, preventing stale global interceptors if a rebuild fires while the menu is open -
Fixed sidebar visibility loss:
syncCanvasLevelnon-iTerm2 branch changed fromwindowLevels.normaltowindowLevels.floating - 1—normallevel can be buried behind iTerm2 windows on macOS Sonoma/Sequoia;floating - 1stays above most content while still yielding focus
2026-06-02
-
Fixed
toggleSidebarshow bug: removed manualsidebarCanvas:show()afterrefreshLayout()— the canvas show is now handled by the debounced_doBuildSidebar(which fires 50ms later), preventing the show from hitting a stale or nil canvas reference before the rebuild completes -
Fixed
_doBuildSidebar: movedcanvas:show()to aftersyncCanvasLevel()so the level is set before the canvas renders, preventing it from being buried behind iTerm windows on macOS -
Fixed syntax error in
getOpenPRForWindow: args table ({"-c", ...}) was being passed as 3rd arg tohs.timer.doAfterinstead ofhs.task.new— same pattern fix asgetGitBranchForPath -
Added
_sidebarEnabledguard tosyncCanvasLevelfor safety; addedsidebarCanvasnil guard intoggleSidebarshow branch to prevent potential crash -
Fixed
toggleSidebarbug: on hide, iTerm windows now expand left to fill the vacated sidebar area; on show, sidebar position is read directly from the existing canvas frame instead of using a broken heuristic that subtractedsidebarWidthfrom iTerm's current X -
Fixed right-click context menu showing duplicate entries (Rename through Move to Bottom appeared twice in
showWindowMenuitems table) -
Fixed
⌘⇧Nnew window hotkey: replacedhs.application.find("iTerm2")withhs.application.get("com.googlecode.iterm2")and added a 0.15s delay before sending⌘Nso iTerm2 has time to gain focus -
Revised default hotkeys: toggle sidebar
⌘⇧A→⌘⇧B, rename⌘⇧W→⌘⇧E, move up/down⌘⇧[/]→⌘⇧↑/↓, move to top/bottom⌘⇧↑/↓→⌘⇧⌥↑/↓; updated context menu shortcut labels to match -
Replaced
hs.dialog.textPromptwith an inline vim-style rename bar at the bottom of the sidebar — type⌘⇧Wor right-click → Rename to show the bar,Returnto commit,Escapeto cancel,⌘Vto paste,⌦to clear -
Rename bar: 40-char max length with visual
[max]nudge,▏slim cursor, 6px bottom margin, paste strips newlines -
Added rename state management (
startRenameMode,commitRename,cancelRenameMode) with globalhs.eventtapkey capture and a 0.5s blinking cursor -
Added
_saveCustomNamehelper shared by both dialog and bar rename paths -
Added
BAR_H = 18constant reserving space for the rename bar at the bottom of the canvas -
Rename mode auto-cancels on: mouse click, iTerm2 deactivation, screen change, or
stop() -
Fixed clicks being swallowed by sidebar canvas when a floating app (e.g. Calendar) overlapped it: disabled
canvasMouseEvents(sidebar no longer captures clicks at the OS level) -
Removed redundant
mouseCallback— all click handling is now done by_leftClickTap/_rightClickTapeventtaps -
Changed
_leftClickTapguard fromfrontmostApplication()to walkinghs.window.orderedWindows()— finds the first window actually covering the click coordinate, so clicks pass through to floating apps above the canvas at that exact point -
Fixed
bringWindowToFrontnot updating button colors immediately on keyboard-triggered focus switching (e.g. ⌥⌘↑/↓) — now paints colors synchronously, matching the click-path behavior -
Fixed canvas visibility bug: hide sidebar when all iTerm windows are closed/minimized, re-show when windows return
-
Added
windowMinimized/windowUnminimizedsubscriptions to trigger rebuilds on minimize/unminimize -
Added Git workspace / worktree support — parses PR numbers from iTerm2 title bar (
parsePRFromTitle), detects worktree context fromgit worktree list --porcelain(_gitWsNameCache), displays workspace-aware Line 3 with color-coded states, short-circuits gh CLI when PR is already in the title -
Right-click handling moved to narrow
_rightClickTapeventtap that only fires when iTerm2 is already frontmost -
Added
_leftClickTapeventtap for fast left-click interception (fires before canvas mouseCallback, paints colors + focuses inline in eventtap) -
_leftClickTapswallows non-button sidebar clicks only when iTerm2 is frontmost — prevents click stealing from other apps overlapping the sidebar area -
Stripped
mouseCallbackdown tohandleSidebarClickonly — left-click handling done entirely by_leftClickTap -
windowFocusedsubscriber now paints button colors immediately viaelementAttribute(no debounce wait) -
Removed
_levelPollTimer(1s syncCanvasLevel poll) — now fully reactive via_appWatcher -
bringWindowToFrontcallsapp:activate()beforewin:raise()/win:focus()for faster cold-start focus switching -
Removed
buildSidebarcall frombringWindowToFront(redundant — windowFocused handles rebuild) -
Removed
handleWindowMoveOrResizefromwindowFocusedsubscriber (focus changes don't move windows, was causing 300ms debounced stall / beach ball) -
Added 100ms TTL cache to
getITermWindows()to avoid repeatedhs.window.allWindows()calls during rapid sequential operations -
Replaced
hs.choosercontext menu with a canvas-based popup menu (showWindowMenu) — avoids the chooser UI and provides native-feeling hover highlight and click handling viahs.eventtap -
Removed
iTerm Settingsmenu item (theshowPreferencesTipoverlay andpreferences_tip.pngasset) -
Added
_menuCanvas/_menuEventTap/_menuKeyTapstate fields for the new context menu; cleaned up instop()andinit() -
Added iTerm2 configuration section to README documenting the "Reuse previous session's directory" setting
-
Added keyboard shortcut labels (⌘⇧W, ⌘⇧[, etc.) to each canvas context menu item, right-aligned in dimmer text