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.
Files changed (3) hide show
  1. package/README.md +33 -30
  2. package/init.lua +295 -60
  3. 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/). Designed for managing multiple Claude Code terminal sessions.
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
  ![Claude Dock](https://img.shields.io/badge/macOS-Hammerspoon-blue)
6
6
  [![npm version](https://img.shields.io/npm/v/claude-dock.svg)](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 Claude** - New terminals automatically run `claude` command
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 empty
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 terminal |
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 `claude`
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 `init.lua` to customize:
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
- -- Configuration
89
- local slotWidth = 140 -- Width of each slot
90
- local slotHeight = 60 -- Height of each slot
91
- local gap = 8 -- Gap between slots
92
- local margin = 10 -- Dock padding
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
- ### Using a Different Terminal
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
- To use iTerm instead of Terminal.app, modify the `onSlotClick` function:
108
+ ### Other Options
100
109
 
101
110
  ```lua
102
- -- Change this:
103
- hs.applescript([[
104
- tell application "Terminal"
105
- do script "claude"
106
- activate
107
- end tell
108
- ]])
109
-
110
- -- To this:
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 Code sessions
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 = 5, -- bg, border, title, status, notification badge
17
- baseElements = 6, -- dock bg + border + help btn + help text + minimize btn + minimize text
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 leftButtonWidth + (config.slotWidth * slotCount) + (config.gap * (slotCount - 1)) + config.gap + rightButtonWidth + (config.margin * 2)
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
- if buttonId == "minBtn" or buttonId == "helpBtn" then
221
- -- Left side utility buttons
222
- btnX = dockFrame.x + config.margin
223
- btnWidth = config.utilityButtonWidth
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
- -- Right side add button
226
- btnX = dockFrame.x + config.margin + config.utilityButtonWidth + config.gap + (slotCount * config.slotWidth) + (slotCount * config.gap)
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 "Terminal"
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 "Terminal"
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 visibility
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 config.colors.notificationBadge
420
- or { red = 0, green = 0, blue = 0, alpha = 0 }
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 Claude Terminal",
635
+ "New " .. agent.name .. " Terminal",
507
636
  "Enter a name for this terminal:",
508
- "Claude " .. slotIndex,
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 ("Claude " .. slotIndex)
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 "claude"
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
- -- Utility buttons (left side, stacked: help on top, minimize on bottom)
574
- local utilBtnX = config.margin
575
- local btnGap = 4
576
- local btnHeight = (config.slotHeight - btnGap) / 2
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
- -- Help button (top)
579
- local helpBtnY = config.margin
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 = utilBtnX, y = helpBtnY, w = config.utilityButtonWidth, h = btnHeight },
751
+ frame = { x = minBtnX, y = utilBtnY, w = minBtnWidth, h = utilBtnSize },
584
752
  roundedRectRadii = { xRadius = 6, yRadius = 6 },
585
- fillColor = config.colors.helpBtnBg,
753
+ fillColor = config.colors.minBtnBg,
586
754
  trackMouseUp = true,
587
755
  trackMouseEnterExit = true,
588
- id = "helpBtn",
756
+ id = "minBtn",
589
757
  })
590
758
  dock:appendElements({
591
759
  type = "text",
592
- frame = { x = utilBtnX, y = helpBtnY + 4, w = config.utilityButtonWidth, h = btnHeight },
593
- text = "?",
760
+ frame = { x = minBtnX, y = utilBtnY + 2, w = minBtnWidth, h = utilBtnSize },
761
+ text = "Hide",
594
762
  textAlignment = "center",
595
- textColor = config.colors.helpBtnText,
596
- textSize = 16,
763
+ textColor = config.colors.minBtnText,
764
+ textSize = 11,
597
765
  textFont = ".AppleSystemUIFontBold",
598
766
  trackMouseUp = true,
599
- id = "helpBtn",
767
+ id = "minBtn",
600
768
  })
601
769
 
602
- -- Minimize button (bottom)
603
- local minBtnY = config.margin + btnHeight + btnGap
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 = utilBtnX, y = minBtnY, w = config.utilityButtonWidth, h = btnHeight },
776
+ frame = { x = helpBtnX, y = utilBtnY, w = helpBtnWidth, h = utilBtnSize },
608
777
  roundedRectRadii = { xRadius = 6, yRadius = 6 },
609
- fillColor = config.colors.minBtnBg,
778
+ fillColor = config.colors.helpBtnBg,
610
779
  trackMouseUp = true,
611
780
  trackMouseEnterExit = true,
612
- id = "minBtn",
781
+ id = "helpBtn",
613
782
  })
614
783
  dock:appendElements({
615
784
  type = "text",
616
- frame = { x = utilBtnX, y = minBtnY + 4, w = config.utilityButtonWidth, h = btnHeight },
617
- text = "",
785
+ frame = { x = helpBtnX, y = utilBtnY + 2, w = helpBtnWidth, h = utilBtnSize },
786
+ text = "Help",
618
787
  textAlignment = "center",
619
- textColor = config.colors.minBtnText,
620
- textSize = 16,
621
- textFont = ".AppleSystemUIFont",
788
+ textColor = config.colors.helpBtnText,
789
+ textSize = 11,
790
+ textFont = ".AppleSystemUIFontBold",
622
791
  trackMouseUp = true,
623
- id = "minBtn",
792
+ id = "helpBtn",
624
793
  })
625
794
 
626
- -- Slots (offset by utility button)
627
- local slotsStartX = config.margin + config.utilityButtonWidth + config.gap
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 = config.margin, w = config.slotWidth, h = config.slotHeight },
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 = config.margin, w = config.slotWidth, h = config.slotHeight },
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 = config.margin + 8, w = config.slotWidth - 12, h = 24 },
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 = config.margin + 32, w = config.slotWidth - 12, h = 20 },
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 of slot, overlapping edge like app badges)
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 = slotX + config.slotWidth - badgeSize/3, y = config.margin + badgeSize/3 },
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 = config.margin, w = config.addButtonWidth, h = config.slotHeight },
878
+ frame = { x = addBtnX, y = slotY, w = config.addButtonWidth, h = config.slotHeight },
687
879
  roundedRectRadii = { xRadius = 10, yRadius = 10 },
688
- fillColor = config.colors.addBtnBg,
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 = config.margin + 13, w = config.addButtonWidth, h = 30 },
887
+ frame = { x = addBtnX, y = slotY + 13, w = config.addButtonWidth, h = 30 },
696
888
  text = "+",
697
889
  textAlignment = "center",
698
- textColor = config.colors.addBtnText,
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.2.0",
4
- "description": "A lightweight terminal dock for macOS to manage Claude Code sessions",
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",