claude-dock 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -30
- package/init.lua +295 -60
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# Claude Dock
|
|
2
2
|
|
|
3
|
-
A lightweight, expandable terminal dock for macOS built with [Hammerspoon](https://www.hammerspoon.org/).
|
|
3
|
+
A lightweight, expandable terminal dock for macOS built with [Hammerspoon](https://www.hammerspoon.org/). Manage multiple AI coding agent sessions - **Claude Code**, **Amp**, and **Codex**.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
[](https://www.npmjs.com/package/claude-dock)
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
+
- **Multi-agent support** - Works with Claude Code, Sourcegraph Amp, and OpenAI Codex
|
|
10
11
|
- **Expandable dock** - Start with 3 slots, add more with "+" button or hotkey
|
|
11
12
|
- **Terminal management** - Each slot tracks a specific terminal window
|
|
12
|
-
- **Auto-launch
|
|
13
|
+
- **Auto-launch** - New terminals automatically run your configured agent
|
|
13
14
|
- **Custom naming** - Name your terminals for easy identification
|
|
14
|
-
- **Visual status** - See which terminals are active, minimized, or
|
|
15
|
+
- **Visual status** - See which terminals are active, minimized, or on other spaces
|
|
16
|
+
- **Notification badges** - Red dot appears when a terminal has activity while unfocused
|
|
15
17
|
- **Quick access** - Click to focus/unminimize terminals
|
|
16
18
|
- **Keyboard shortcuts** - Full hotkey support
|
|
17
19
|
|
|
@@ -61,6 +63,7 @@ System Settings → Privacy & Security → Accessibility → Enable Hammerspoon
|
|
|
61
63
|
|----------|--------|
|
|
62
64
|
| `Cmd+Option+T` | Toggle dock visibility |
|
|
63
65
|
| `Cmd+Option+N` | Add new slot + launch terminal |
|
|
66
|
+
| `Cmd+Option+M` | Minimize all terminals |
|
|
64
67
|
| `Cmd+Option+R` | Reload configuration |
|
|
65
68
|
| `Option+Click` | Rename a slot |
|
|
66
69
|
|
|
@@ -70,11 +73,12 @@ System Settings → Privacy & Security → Accessibility → Enable Hammerspoon
|
|
|
70
73
|
|-------|--------|
|
|
71
74
|
| Gray | Empty - click to open new terminal |
|
|
72
75
|
| Green | Active terminal |
|
|
73
|
-
| Blue | Minimized
|
|
76
|
+
| Blue | Minimized or on other space |
|
|
77
|
+
| Red dot | Terminal has new activity |
|
|
74
78
|
|
|
75
79
|
### Click Actions
|
|
76
80
|
|
|
77
|
-
- **Click empty slot** - Prompts for name, opens terminal, runs
|
|
81
|
+
- **Click empty slot** - Prompts for name, opens terminal, runs agent
|
|
78
82
|
- **Click active slot** - Focuses that terminal window
|
|
79
83
|
- **Click minimized slot** - Unminimizes and focuses
|
|
80
84
|
- **Click "+" button** - Adds new slot and launches terminal
|
|
@@ -82,38 +86,37 @@ System Settings → Privacy & Security → Accessibility → Enable Hammerspoon
|
|
|
82
86
|
|
|
83
87
|
## Configuration
|
|
84
88
|
|
|
85
|
-
Edit
|
|
89
|
+
Edit `~/.hammerspoon/init.lua` to customize.
|
|
90
|
+
|
|
91
|
+
### Changing the Agent
|
|
92
|
+
|
|
93
|
+
By default, claude-dock launches `claude`. To use a different agent:
|
|
86
94
|
|
|
87
95
|
```lua
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
local bottomOffset = 5 -- Distance from screen bottom
|
|
94
|
-
local slotCount = 3 -- Initial number of slots
|
|
96
|
+
local config = {
|
|
97
|
+
-- Agent to launch: "claude", "amp", or "codex"
|
|
98
|
+
agent = "amp", -- Change to your preferred agent
|
|
99
|
+
...
|
|
100
|
+
}
|
|
95
101
|
```
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
Supported agents:
|
|
104
|
+
- `"claude"` - [Claude Code](https://claude.ai/code) (default)
|
|
105
|
+
- `"amp"` - [Sourcegraph Amp](https://ampcode.com/)
|
|
106
|
+
- `"codex"` - [OpenAI Codex](https://openai.com/codex/)
|
|
98
107
|
|
|
99
|
-
|
|
108
|
+
### Other Options
|
|
100
109
|
|
|
101
110
|
```lua
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
hs.applescript([[
|
|
112
|
-
tell application "iTerm"
|
|
113
|
-
create window with default profile command "claude"
|
|
114
|
-
activate
|
|
115
|
-
end tell
|
|
116
|
-
]])
|
|
111
|
+
local config = {
|
|
112
|
+
agent = "claude", -- Which AI agent to launch
|
|
113
|
+
slotWidth = 140, -- Width of each slot
|
|
114
|
+
slotHeight = 60, -- Height of each slot
|
|
115
|
+
gap = 8, -- Gap between slots
|
|
116
|
+
margin = 10, -- Dock padding
|
|
117
|
+
bottomOffset = 5, -- Distance from screen bottom
|
|
118
|
+
initialSlots = 3, -- Starting number of slots
|
|
119
|
+
}
|
|
117
120
|
```
|
|
118
121
|
|
|
119
122
|
## Running Tests
|
package/init.lua
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
-- Claude Dock: Terminal dock for managing Claude
|
|
1
|
+
-- Claude Dock: Terminal dock for managing AI coding agent sessions (Claude, Amp, Codex)
|
|
2
2
|
-- https://github.com/YOUR_USERNAME/claude-dock
|
|
3
3
|
|
|
4
4
|
require("hs.ipc")
|
|
5
5
|
|
|
6
6
|
-- Configuration
|
|
7
7
|
local config = {
|
|
8
|
+
-- Agent to launch: "claude", "amp", or "codex"
|
|
9
|
+
agent = "claude",
|
|
8
10
|
slotWidth = 140,
|
|
9
11
|
slotHeight = 60,
|
|
10
12
|
gap = 8,
|
|
@@ -13,8 +15,8 @@ local config = {
|
|
|
13
15
|
addButtonWidth = 40,
|
|
14
16
|
utilityButtonWidth = 28,
|
|
15
17
|
initialSlots = 3,
|
|
16
|
-
elementsPerSlot =
|
|
17
|
-
baseElements =
|
|
18
|
+
elementsPerSlot = 7, -- bg, border, title, status, badge glow outer, badge glow inner, badge
|
|
19
|
+
baseElements = 12, -- dock bg + border + 6 tab elements + 4 utility btn elements
|
|
18
20
|
windowCaptureDelay = 0.3,
|
|
19
21
|
windowCaptureRetries = 5,
|
|
20
22
|
colors = {
|
|
@@ -35,8 +37,37 @@ local config = {
|
|
|
35
37
|
notificationBadge = { red = 1, green = 0.3, blue = 0.3, alpha = 1 },
|
|
36
38
|
},
|
|
37
39
|
notificationBadgeSize = 12,
|
|
40
|
+
tabHeight = 28,
|
|
41
|
+
tabWidth = 60,
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
-- Supported agents (order matters for tab display)
|
|
45
|
+
local agentOrder = { "claude", "amp", "codex" }
|
|
46
|
+
local agents = {
|
|
47
|
+
claude = {
|
|
48
|
+
command = "claude",
|
|
49
|
+
name = "Claude",
|
|
50
|
+
shortName = "Claude",
|
|
51
|
+
color = { red = 0.76, green = 0.37, blue = 0.24, alpha = 1 }, -- #C15F3C
|
|
52
|
+
},
|
|
53
|
+
amp = {
|
|
54
|
+
command = "amp",
|
|
55
|
+
name = "Amp",
|
|
56
|
+
shortName = "Amp",
|
|
57
|
+
color = { red = 0.6, green = 0.2, blue = 0.8, alpha = 1 }, -- Purple
|
|
58
|
+
},
|
|
59
|
+
codex = {
|
|
60
|
+
command = "codex",
|
|
61
|
+
name = "Codex",
|
|
62
|
+
shortName = "Codex",
|
|
63
|
+
color = { red = 0.0, green = 0.65, blue = 0.52, alpha = 1 }, -- OpenAI teal #00A67E
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
local function getAgent()
|
|
68
|
+
return agents[config.agent] or agents.claude
|
|
69
|
+
end
|
|
70
|
+
|
|
40
71
|
-- State
|
|
41
72
|
local slotCount = config.initialSlots
|
|
42
73
|
local slots = {}
|
|
@@ -44,6 +75,8 @@ local dock = nil
|
|
|
44
75
|
local tooltip = nil
|
|
45
76
|
local helpPanel = nil
|
|
46
77
|
local macOSDockAtBottom = false
|
|
78
|
+
local pulseTimer = nil
|
|
79
|
+
local pulsePhase = 0
|
|
47
80
|
|
|
48
81
|
-- Check macOS dock position
|
|
49
82
|
local function getMacOSDockPosition()
|
|
@@ -80,6 +113,10 @@ local function cleanup()
|
|
|
80
113
|
updateTimer:stop()
|
|
81
114
|
updateTimer = nil
|
|
82
115
|
end
|
|
116
|
+
if pulseTimer then
|
|
117
|
+
pulseTimer:stop()
|
|
118
|
+
pulseTimer = nil
|
|
119
|
+
end
|
|
83
120
|
if screenWatcher then
|
|
84
121
|
screenWatcher:stop()
|
|
85
122
|
screenWatcher = nil
|
|
@@ -110,14 +147,12 @@ initSlots()
|
|
|
110
147
|
|
|
111
148
|
-- Calculate dock dimensions
|
|
112
149
|
local function getDockWidth()
|
|
113
|
-
-- Left: small utility button (minimize), Right: add button
|
|
114
|
-
local leftButtonWidth = config.utilityButtonWidth + config.gap
|
|
115
150
|
local rightButtonWidth = config.addButtonWidth
|
|
116
|
-
return
|
|
151
|
+
return (config.slotWidth * slotCount) + (config.gap * (slotCount - 1)) + config.gap + rightButtonWidth + (config.margin * 2)
|
|
117
152
|
end
|
|
118
153
|
|
|
119
154
|
local function getDockHeight()
|
|
120
|
-
return config.slotHeight + (config.margin * 2)
|
|
155
|
+
return config.tabHeight + config.slotHeight + (config.margin * 2)
|
|
121
156
|
end
|
|
122
157
|
|
|
123
158
|
local function getDockFrame()
|
|
@@ -165,6 +200,9 @@ local function findSlotByWindowId(windowId)
|
|
|
165
200
|
return nil
|
|
166
201
|
end
|
|
167
202
|
|
|
203
|
+
-- Forward declaration for pulse animation
|
|
204
|
+
local startPulseAnimation
|
|
205
|
+
|
|
168
206
|
-- Set notification for a slot (only if window is not focused)
|
|
169
207
|
local function setSlotNotification(slotIndex)
|
|
170
208
|
local slot = slots[slotIndex]
|
|
@@ -177,6 +215,7 @@ local function setSlotNotification(slotIndex)
|
|
|
177
215
|
if not focusedWin or focusedWin:id() ~= slot.windowId then
|
|
178
216
|
slot.hasNotification = true
|
|
179
217
|
updateSlotDisplay(slotIndex)
|
|
218
|
+
if startPulseAnimation then startPulseAnimation() end
|
|
180
219
|
end
|
|
181
220
|
end
|
|
182
221
|
end
|
|
@@ -216,14 +255,23 @@ local function showButtonTooltip(text, buttonId)
|
|
|
216
255
|
local tipWidth = 50
|
|
217
256
|
local tipHeight = 24
|
|
218
257
|
local btnX, btnWidth
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
local dockWidth = getDockWidth()
|
|
259
|
+
local utilBtnSize = config.tabHeight - 8
|
|
260
|
+
|
|
261
|
+
if buttonId == "minBtn" then
|
|
262
|
+
-- Top right minimize button
|
|
263
|
+
local minBtnWidth = 32
|
|
264
|
+
btnX = dockFrame.x + dockWidth - config.margin - minBtnWidth
|
|
265
|
+
btnWidth = minBtnWidth
|
|
266
|
+
elseif buttonId == "helpBtn" then
|
|
267
|
+
-- Top right help button (left of minimize)
|
|
268
|
+
local minBtnWidth = 32
|
|
269
|
+
local helpBtnWidth = 36
|
|
270
|
+
btnX = dockFrame.x + dockWidth - config.margin - minBtnWidth - 4 - helpBtnWidth
|
|
271
|
+
btnWidth = helpBtnWidth
|
|
224
272
|
elseif buttonId == "addBtn" then
|
|
225
|
-
--
|
|
226
|
-
btnX = dockFrame.x + config.margin +
|
|
273
|
+
-- Bottom right add button
|
|
274
|
+
btnX = dockFrame.x + config.margin + (slotCount * config.slotWidth) + (slotCount * config.gap)
|
|
227
275
|
btnWidth = config.addButtonWidth
|
|
228
276
|
end
|
|
229
277
|
|
|
@@ -378,8 +426,9 @@ local function updateSlotDisplay(slotIndex)
|
|
|
378
426
|
local title, status, bgColor
|
|
379
427
|
local win = getWindow(slot.windowId)
|
|
380
428
|
|
|
429
|
+
local agent = getAgent()
|
|
381
430
|
if win then
|
|
382
|
-
title = slot.customName or getWindowTitle(win) or
|
|
431
|
+
title = slot.customName or getWindowTitle(win) or agent.name
|
|
383
432
|
if win:isMinimized() then
|
|
384
433
|
status = "(minimized)"
|
|
385
434
|
bgColor = config.colors.slotMinimized
|
|
@@ -389,7 +438,7 @@ local function updateSlotDisplay(slotIndex)
|
|
|
389
438
|
end
|
|
390
439
|
elseif slot.windowId then
|
|
391
440
|
-- Window exists but not visible (probably on another space)
|
|
392
|
-
title = slot.customName or
|
|
441
|
+
title = slot.customName or agent.name
|
|
393
442
|
status = "(other space)"
|
|
394
443
|
bgColor = config.colors.slotMinimized
|
|
395
444
|
else
|
|
@@ -413,11 +462,27 @@ local function updateSlotDisplay(slotIndex)
|
|
|
413
462
|
if dock[baseIdx + 3] then
|
|
414
463
|
dock[baseIdx + 3].text = status
|
|
415
464
|
end
|
|
416
|
-
-- Update notification badge
|
|
465
|
+
-- Update notification badge with glow
|
|
466
|
+
local badgeColor = config.colors.notificationBadge
|
|
467
|
+
local hidden = { red = 0, green = 0, blue = 0, alpha = 0 }
|
|
468
|
+
|
|
469
|
+
-- Outer glow (baseIdx + 4)
|
|
417
470
|
if dock[baseIdx + 4] then
|
|
418
471
|
dock[baseIdx + 4].fillColor = slot.hasNotification
|
|
419
|
-
and
|
|
420
|
-
or
|
|
472
|
+
and { red = badgeColor.red, green = badgeColor.green, blue = badgeColor.blue, alpha = 0.2 }
|
|
473
|
+
or hidden
|
|
474
|
+
end
|
|
475
|
+
-- Inner glow (baseIdx + 5)
|
|
476
|
+
if dock[baseIdx + 5] then
|
|
477
|
+
dock[baseIdx + 5].fillColor = slot.hasNotification
|
|
478
|
+
and { red = badgeColor.red, green = badgeColor.green, blue = badgeColor.blue, alpha = 0.4 }
|
|
479
|
+
or hidden
|
|
480
|
+
end
|
|
481
|
+
-- Main badge (baseIdx + 6)
|
|
482
|
+
if dock[baseIdx + 6] then
|
|
483
|
+
dock[baseIdx + 6].fillColor = slot.hasNotification
|
|
484
|
+
and badgeColor
|
|
485
|
+
or hidden
|
|
421
486
|
end
|
|
422
487
|
end
|
|
423
488
|
|
|
@@ -427,6 +492,69 @@ updateAllSlots = function()
|
|
|
427
492
|
end
|
|
428
493
|
end
|
|
429
494
|
|
|
495
|
+
-- Pulse animation for notification badges
|
|
496
|
+
local function updatePulse()
|
|
497
|
+
if not dock then return end
|
|
498
|
+
|
|
499
|
+
pulsePhase = pulsePhase + 0.15
|
|
500
|
+
if pulsePhase > math.pi * 2 then
|
|
501
|
+
pulsePhase = 0
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
-- Pulsing multiplier: oscillates between 0.5 and 1.0
|
|
505
|
+
local pulse = 0.75 + 0.25 * math.sin(pulsePhase)
|
|
506
|
+
|
|
507
|
+
local badgeColor = config.colors.notificationBadge
|
|
508
|
+
local hidden = { red = 0, green = 0, blue = 0, alpha = 0 }
|
|
509
|
+
|
|
510
|
+
local hasAnyNotification = false
|
|
511
|
+
for i = 1, slotCount do
|
|
512
|
+
local slot = slots[i]
|
|
513
|
+
if slot and slot.hasNotification then
|
|
514
|
+
hasAnyNotification = true
|
|
515
|
+
local baseIdx = config.baseElements + 1 + ((i - 1) * config.elementsPerSlot)
|
|
516
|
+
|
|
517
|
+
-- Outer glow pulses
|
|
518
|
+
if dock[baseIdx + 4] then
|
|
519
|
+
dock[baseIdx + 4].fillColor = {
|
|
520
|
+
red = badgeColor.red,
|
|
521
|
+
green = badgeColor.green,
|
|
522
|
+
blue = badgeColor.blue,
|
|
523
|
+
alpha = 0.2 * pulse
|
|
524
|
+
}
|
|
525
|
+
end
|
|
526
|
+
-- Inner glow pulses
|
|
527
|
+
if dock[baseIdx + 5] then
|
|
528
|
+
dock[baseIdx + 5].fillColor = {
|
|
529
|
+
red = badgeColor.red,
|
|
530
|
+
green = badgeColor.green,
|
|
531
|
+
blue = badgeColor.blue,
|
|
532
|
+
alpha = 0.5 * pulse
|
|
533
|
+
}
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
-- Stop timer if no notifications
|
|
539
|
+
if not hasAnyNotification and pulseTimer then
|
|
540
|
+
pulseTimer:stop()
|
|
541
|
+
pulseTimer = nil
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
startPulseAnimation = function()
|
|
546
|
+
if not pulseTimer then
|
|
547
|
+
pulseTimer = hs.timer.doEvery(0.05, updatePulse)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
local function stopPulseAnimation()
|
|
552
|
+
if pulseTimer then
|
|
553
|
+
pulseTimer:stop()
|
|
554
|
+
pulseTimer = nil
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
430
558
|
-- Rename a slot
|
|
431
559
|
local function renameSlot(slotIndex)
|
|
432
560
|
local slot = slots[slotIndex]
|
|
@@ -502,10 +630,11 @@ local function onSlotClick(slotIndex, isOptionClick)
|
|
|
502
630
|
end
|
|
503
631
|
win:focus()
|
|
504
632
|
else
|
|
633
|
+
local agent = getAgent()
|
|
505
634
|
local button, newName = hs.dialog.textPrompt(
|
|
506
|
-
"New
|
|
635
|
+
"New " .. agent.name .. " Terminal",
|
|
507
636
|
"Enter a name for this terminal:",
|
|
508
|
-
"
|
|
637
|
+
agent.shortName .. " " .. slotIndex,
|
|
509
638
|
"Create", "Cancel"
|
|
510
639
|
)
|
|
511
640
|
|
|
@@ -513,12 +642,12 @@ local function onSlotClick(slotIndex, isOptionClick)
|
|
|
513
642
|
return
|
|
514
643
|
end
|
|
515
644
|
|
|
516
|
-
slot.customName = (newName and newName ~= "") and newName or ("
|
|
645
|
+
slot.customName = (newName and newName ~= "") and newName or (agent.shortName .. " " .. slotIndex)
|
|
517
646
|
slot.pending = true
|
|
518
647
|
|
|
519
648
|
hs.applescript([[
|
|
520
649
|
tell application "Terminal"
|
|
521
|
-
do script "
|
|
650
|
+
do script "]] .. agent.command .. [["
|
|
522
651
|
activate
|
|
523
652
|
end tell
|
|
524
653
|
]])
|
|
@@ -570,68 +699,109 @@ createDock = function()
|
|
|
570
699
|
frame = { x = 0, y = 0, w = "100%", h = "100%" },
|
|
571
700
|
})
|
|
572
701
|
|
|
573
|
-
--
|
|
574
|
-
local
|
|
575
|
-
local
|
|
576
|
-
|
|
702
|
+
-- Agent tabs (top left)
|
|
703
|
+
local tabY = 6
|
|
704
|
+
local tabStartX = config.margin
|
|
705
|
+
for i, agentKey in ipairs(agentOrder) do
|
|
706
|
+
local agent = agents[agentKey]
|
|
707
|
+
local tabX = tabStartX + ((i - 1) * (config.tabWidth + 4))
|
|
708
|
+
local isSelected = (config.agent == agentKey)
|
|
709
|
+
|
|
710
|
+
-- Tab background
|
|
711
|
+
dock:appendElements({
|
|
712
|
+
type = "rectangle",
|
|
713
|
+
action = "fill",
|
|
714
|
+
frame = { x = tabX, y = tabY, w = config.tabWidth, h = config.tabHeight - 8 },
|
|
715
|
+
roundedRectRadii = { xRadius = 6, yRadius = 6 },
|
|
716
|
+
fillColor = isSelected and agent.color or { red = 0.15, green = 0.15, blue = 0.15, alpha = 1 },
|
|
717
|
+
trackMouseUp = true,
|
|
718
|
+
id = "tab_" .. agentKey,
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
-- Tab text
|
|
722
|
+
dock:appendElements({
|
|
723
|
+
type = "text",
|
|
724
|
+
frame = { x = tabX, y = tabY + 2, w = config.tabWidth, h = config.tabHeight - 8 },
|
|
725
|
+
text = agent.name,
|
|
726
|
+
textAlignment = "center",
|
|
727
|
+
textColor = isSelected
|
|
728
|
+
and { red = 1, green = 1, blue = 1, alpha = 1 }
|
|
729
|
+
or { red = 0.5, green = 0.5, blue = 0.5, alpha = 1 },
|
|
730
|
+
textSize = 12,
|
|
731
|
+
textFont = isSelected and ".AppleSystemUIFontBold" or ".AppleSystemUIFont",
|
|
732
|
+
trackMouseUp = true,
|
|
733
|
+
id = "tab_" .. agentKey,
|
|
734
|
+
})
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
-- Content area starts below tabs
|
|
738
|
+
local contentY = config.tabHeight
|
|
739
|
+
|
|
740
|
+
-- Utility buttons (top right, in tab bar area - same Y as tabs)
|
|
741
|
+
local utilBtnSize = config.tabHeight - 8
|
|
742
|
+
local utilBtnY = 6
|
|
743
|
+
local dockWidth = getDockWidth()
|
|
577
744
|
|
|
578
|
-
--
|
|
579
|
-
local
|
|
745
|
+
-- Minimize button (rightmost)
|
|
746
|
+
local minBtnWidth = 32
|
|
747
|
+
local minBtnX = dockWidth - config.margin - minBtnWidth
|
|
580
748
|
dock:appendElements({
|
|
581
749
|
type = "rectangle",
|
|
582
750
|
action = "fill",
|
|
583
|
-
frame = { x =
|
|
751
|
+
frame = { x = minBtnX, y = utilBtnY, w = minBtnWidth, h = utilBtnSize },
|
|
584
752
|
roundedRectRadii = { xRadius = 6, yRadius = 6 },
|
|
585
|
-
fillColor = config.colors.
|
|
753
|
+
fillColor = config.colors.minBtnBg,
|
|
586
754
|
trackMouseUp = true,
|
|
587
755
|
trackMouseEnterExit = true,
|
|
588
|
-
id = "
|
|
756
|
+
id = "minBtn",
|
|
589
757
|
})
|
|
590
758
|
dock:appendElements({
|
|
591
759
|
type = "text",
|
|
592
|
-
frame = { x =
|
|
593
|
-
text = "
|
|
760
|
+
frame = { x = minBtnX, y = utilBtnY + 2, w = minBtnWidth, h = utilBtnSize },
|
|
761
|
+
text = "Hide",
|
|
594
762
|
textAlignment = "center",
|
|
595
|
-
textColor = config.colors.
|
|
596
|
-
textSize =
|
|
763
|
+
textColor = config.colors.minBtnText,
|
|
764
|
+
textSize = 11,
|
|
597
765
|
textFont = ".AppleSystemUIFontBold",
|
|
598
766
|
trackMouseUp = true,
|
|
599
|
-
id = "
|
|
767
|
+
id = "minBtn",
|
|
600
768
|
})
|
|
601
769
|
|
|
602
|
-
--
|
|
603
|
-
local
|
|
770
|
+
-- Help button (left of minimize)
|
|
771
|
+
local helpBtnWidth = 36
|
|
772
|
+
local helpBtnX = minBtnX - helpBtnWidth - 4
|
|
604
773
|
dock:appendElements({
|
|
605
774
|
type = "rectangle",
|
|
606
775
|
action = "fill",
|
|
607
|
-
frame = { x =
|
|
776
|
+
frame = { x = helpBtnX, y = utilBtnY, w = helpBtnWidth, h = utilBtnSize },
|
|
608
777
|
roundedRectRadii = { xRadius = 6, yRadius = 6 },
|
|
609
|
-
fillColor = config.colors.
|
|
778
|
+
fillColor = config.colors.helpBtnBg,
|
|
610
779
|
trackMouseUp = true,
|
|
611
780
|
trackMouseEnterExit = true,
|
|
612
|
-
id = "
|
|
781
|
+
id = "helpBtn",
|
|
613
782
|
})
|
|
614
783
|
dock:appendElements({
|
|
615
784
|
type = "text",
|
|
616
|
-
frame = { x =
|
|
617
|
-
text = "
|
|
785
|
+
frame = { x = helpBtnX, y = utilBtnY + 2, w = helpBtnWidth, h = utilBtnSize },
|
|
786
|
+
text = "Help",
|
|
618
787
|
textAlignment = "center",
|
|
619
|
-
textColor = config.colors.
|
|
620
|
-
textSize =
|
|
621
|
-
textFont = ".
|
|
788
|
+
textColor = config.colors.helpBtnText,
|
|
789
|
+
textSize = 11,
|
|
790
|
+
textFont = ".AppleSystemUIFontBold",
|
|
622
791
|
trackMouseUp = true,
|
|
623
|
-
id = "
|
|
792
|
+
id = "helpBtn",
|
|
624
793
|
})
|
|
625
794
|
|
|
626
|
-
-- Slots
|
|
627
|
-
local slotsStartX = config.margin
|
|
795
|
+
-- Slots
|
|
796
|
+
local slotsStartX = config.margin
|
|
797
|
+
local slotY = contentY + config.margin
|
|
628
798
|
for i = 1, slotCount do
|
|
629
799
|
local slotX = slotsStartX + ((i - 1) * (config.slotWidth + config.gap))
|
|
630
800
|
|
|
631
801
|
dock:appendElements({
|
|
632
802
|
type = "rectangle",
|
|
633
803
|
action = "fill",
|
|
634
|
-
frame = { x = slotX, y =
|
|
804
|
+
frame = { x = slotX, y = slotY, w = config.slotWidth, h = config.slotHeight },
|
|
635
805
|
roundedRectRadii = { xRadius = 10, yRadius = 10 },
|
|
636
806
|
fillColor = config.colors.slotEmpty,
|
|
637
807
|
trackMouseUp = true,
|
|
@@ -641,7 +811,7 @@ createDock = function()
|
|
|
641
811
|
dock:appendElements({
|
|
642
812
|
type = "rectangle",
|
|
643
813
|
action = "stroke",
|
|
644
|
-
frame = { x = slotX, y =
|
|
814
|
+
frame = { x = slotX, y = slotY, w = config.slotWidth, h = config.slotHeight },
|
|
645
815
|
roundedRectRadii = { xRadius = 10, yRadius = 10 },
|
|
646
816
|
strokeColor = config.colors.slotBorder,
|
|
647
817
|
strokeWidth = 1,
|
|
@@ -649,7 +819,7 @@ createDock = function()
|
|
|
649
819
|
|
|
650
820
|
dock:appendElements({
|
|
651
821
|
type = "text",
|
|
652
|
-
frame = { x = slotX + 6, y =
|
|
822
|
+
frame = { x = slotX + 6, y = slotY + 8, w = config.slotWidth - 12, h = 24 },
|
|
653
823
|
text = "Empty",
|
|
654
824
|
textAlignment = "center",
|
|
655
825
|
textColor = config.colors.textPrimary,
|
|
@@ -659,7 +829,7 @@ createDock = function()
|
|
|
659
829
|
|
|
660
830
|
dock:appendElements({
|
|
661
831
|
type = "text",
|
|
662
|
-
frame = { x = slotX + 6, y =
|
|
832
|
+
frame = { x = slotX + 6, y = slotY + 32, w = config.slotWidth - 12, h = 20 },
|
|
663
833
|
text = "click to open",
|
|
664
834
|
textAlignment = "center",
|
|
665
835
|
textColor = config.colors.textSecondary,
|
|
@@ -667,12 +837,34 @@ createDock = function()
|
|
|
667
837
|
textFont = ".AppleSystemUIFont",
|
|
668
838
|
})
|
|
669
839
|
|
|
670
|
-
-- Notification badge (top-right corner
|
|
840
|
+
-- Notification badge with glow (top-right corner, true corner position)
|
|
671
841
|
local badgeSize = config.notificationBadgeSize
|
|
842
|
+
local badgeCenterX = slotX + config.slotWidth - 4
|
|
843
|
+
local badgeCenterY = slotY + 4
|
|
844
|
+
|
|
845
|
+
-- Outer glow
|
|
672
846
|
dock:appendElements({
|
|
673
847
|
type = "circle",
|
|
674
848
|
action = "fill",
|
|
675
|
-
center = { x =
|
|
849
|
+
center = { x = badgeCenterX, y = badgeCenterY },
|
|
850
|
+
radius = badgeSize,
|
|
851
|
+
fillColor = { red = 0, green = 0, blue = 0, alpha = 0 }, -- Hidden by default
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
-- Inner glow
|
|
855
|
+
dock:appendElements({
|
|
856
|
+
type = "circle",
|
|
857
|
+
action = "fill",
|
|
858
|
+
center = { x = badgeCenterX, y = badgeCenterY },
|
|
859
|
+
radius = badgeSize * 0.75,
|
|
860
|
+
fillColor = { red = 0, green = 0, blue = 0, alpha = 0 }, -- Hidden by default
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
-- Main badge
|
|
864
|
+
dock:appendElements({
|
|
865
|
+
type = "circle",
|
|
866
|
+
action = "fill",
|
|
867
|
+
center = { x = badgeCenterX, y = badgeCenterY },
|
|
676
868
|
radius = badgeSize / 2,
|
|
677
869
|
fillColor = { red = 0, green = 0, blue = 0, alpha = 0 }, -- Hidden by default
|
|
678
870
|
})
|
|
@@ -683,19 +875,19 @@ createDock = function()
|
|
|
683
875
|
dock:appendElements({
|
|
684
876
|
type = "rectangle",
|
|
685
877
|
action = "fill",
|
|
686
|
-
frame = { x = addBtnX, y =
|
|
878
|
+
frame = { x = addBtnX, y = slotY, w = config.addButtonWidth, h = config.slotHeight },
|
|
687
879
|
roundedRectRadii = { xRadius = 10, yRadius = 10 },
|
|
688
|
-
fillColor =
|
|
880
|
+
fillColor = { red = 0.95, green = 0.95, blue = 0.95, alpha = 1 },
|
|
689
881
|
trackMouseUp = true,
|
|
690
882
|
trackMouseEnterExit = true,
|
|
691
883
|
id = "addBtn",
|
|
692
884
|
})
|
|
693
885
|
dock:appendElements({
|
|
694
886
|
type = "text",
|
|
695
|
-
frame = { x = addBtnX, y =
|
|
887
|
+
frame = { x = addBtnX, y = slotY + 13, w = config.addButtonWidth, h = 30 },
|
|
696
888
|
text = "+",
|
|
697
889
|
textAlignment = "center",
|
|
698
|
-
textColor =
|
|
890
|
+
textColor = { red = 0.3, green = 0.3, blue = 0.3, alpha = 1 },
|
|
699
891
|
textSize = 28,
|
|
700
892
|
textFont = ".AppleSystemUIFont",
|
|
701
893
|
trackMouseUp = true,
|
|
@@ -710,6 +902,14 @@ createDock = function()
|
|
|
710
902
|
minimizeAllTerminals()
|
|
711
903
|
elseif id == "helpBtn" then
|
|
712
904
|
showHelpPanel()
|
|
905
|
+
elseif id and id:match("^tab_") then
|
|
906
|
+
local agentKey = id:match("^tab_(.+)$")
|
|
907
|
+
if agentKey and agents[agentKey] then
|
|
908
|
+
config.agent = agentKey
|
|
909
|
+
-- Recreate dock to update tab visuals
|
|
910
|
+
dock:delete()
|
|
911
|
+
createDock()
|
|
912
|
+
end
|
|
713
913
|
elseif id and id:match("^slot") then
|
|
714
914
|
local idx = tonumber(id:match("%d+"))
|
|
715
915
|
if idx then
|
|
@@ -814,6 +1014,7 @@ windowFilter:subscribe(hs.window.filter.windowTitleChanged, function(win)
|
|
|
814
1014
|
if not focusedWin or focusedWin:id() ~= win:id() then
|
|
815
1015
|
slots[slotIndex].hasNotification = true
|
|
816
1016
|
updateSlotDisplay(slotIndex)
|
|
1017
|
+
startPulseAnimation()
|
|
817
1018
|
end
|
|
818
1019
|
end
|
|
819
1020
|
end
|
|
@@ -1135,6 +1336,40 @@ function runTests()
|
|
|
1135
1336
|
assertEqual(clearNotification(9999), false)
|
|
1136
1337
|
end)
|
|
1137
1338
|
|
|
1339
|
+
-- Agent configuration tests
|
|
1340
|
+
test("config has agent field", function()
|
|
1341
|
+
assert(config.agent, "config.agent should exist")
|
|
1342
|
+
end)
|
|
1343
|
+
|
|
1344
|
+
test("agents table has claude, amp, codex", function()
|
|
1345
|
+
assert(agents.claude, "agents.claude should exist")
|
|
1346
|
+
assert(agents.amp, "agents.amp should exist")
|
|
1347
|
+
assert(agents.codex, "agents.codex should exist")
|
|
1348
|
+
end)
|
|
1349
|
+
|
|
1350
|
+
test("each agent has command and name", function()
|
|
1351
|
+
for name, agent in pairs(agents) do
|
|
1352
|
+
assert(agent.command, name .. " should have command")
|
|
1353
|
+
assert(agent.name, name .. " should have name")
|
|
1354
|
+
assert(agent.shortName, name .. " should have shortName")
|
|
1355
|
+
end
|
|
1356
|
+
end)
|
|
1357
|
+
|
|
1358
|
+
test("getAgent returns valid agent", function()
|
|
1359
|
+
local agent = getAgent()
|
|
1360
|
+
assert(agent, "getAgent should return an agent")
|
|
1361
|
+
assert(agent.command, "agent should have command")
|
|
1362
|
+
assert(agent.name, "agent should have name")
|
|
1363
|
+
end)
|
|
1364
|
+
|
|
1365
|
+
test("getAgent falls back to claude for invalid config", function()
|
|
1366
|
+
local originalAgent = config.agent
|
|
1367
|
+
config.agent = "invalid"
|
|
1368
|
+
local agent = getAgent()
|
|
1369
|
+
assertEqual(agent.command, "claude")
|
|
1370
|
+
config.agent = originalAgent
|
|
1371
|
+
end)
|
|
1372
|
+
|
|
1138
1373
|
print("\n=== Results: " .. passed .. " passed, " .. failed .. " failed ===\n")
|
|
1139
1374
|
return failed == 0
|
|
1140
1375
|
end
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dock",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A lightweight terminal dock for macOS to manage Claude Code
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "A lightweight terminal dock for macOS to manage AI coding agent sessions - Claude Code, Amp, and Codex",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-dock": "bin/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"keywords": [
|
|
9
9
|
"claude",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"amp",
|
|
12
|
+
"codex",
|
|
13
|
+
"openai",
|
|
14
|
+
"sourcegraph",
|
|
15
|
+
"ai-coding",
|
|
10
16
|
"hammerspoon",
|
|
11
17
|
"macos",
|
|
12
18
|
"terminal",
|