detached 2bfaca9

fix(arch): address final architecture review findings

commit
2bfaca97f5e024146aeba3486d3bd60c60872f0d
author
brice · · 2026-06-25 15:47 UTC
parent
ec8c3ee
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +142 -118
M cmd/dumber/main.go +2 -2
@@ -801,8 +801,8 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
801801 historyUC.SetHistoryMutationCoordinator(historyRecorderUC)
802802
803803 return &useCases{
804- tabs: usecase.NewManageTabsUseCase(idGenerator, usecase.WithManageTabsLocalPathResolver(localPaths)),
805- panes: usecase.NewManagePanesUseCase(idGenerator, usecase.WithManagePanesLocalPathResolver(localPaths)),
804+ tabs: usecase.NewManageTabsUseCase(idGenerator, localPaths),
805+ panes: usecase.NewManagePanesUseCase(idGenerator, localPaths),
806806 history: historyUC,
807807 favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),
808808 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),
@@ -801,8 +801,8 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
801 historyUC.SetHistoryMutationCoordinator(historyRecorderUC)801 historyUC.SetHistoryMutationCoordinator(historyRecorderUC)
802 802
803 return &useCases{803 return &useCases{
804- tabs: usecase.NewManageTabsUseCase(idGenerator, usecase.WithManageTabsLocalPathResolver(localPaths)),804+ tabs: usecase.NewManageTabsUseCase(idGenerator, localPaths),
805- panes: usecase.NewManagePanesUseCase(idGenerator, usecase.WithManagePanesLocalPathResolver(localPaths)),805+ panes: usecase.NewManagePanesUseCase(idGenerator, localPaths),
806 history: historyUC,806 history: historyUC,
807 favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),807 favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),
808 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),808 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),
M internal/application/usecase/manage_panes.go +18 -25
@@ -75,26 +75,12 @@ type ManagePanesUseCase struct {
7575 normalizer *NavigationURLNormalizer
7676 }
7777
78-// ManagePanesOption configures ManagePanesUseCase.
79-type ManagePanesOption func(*ManagePanesUseCase)
80-
81-// WithManagePanesLocalPathResolver configures local path resolution for initial navigation URLs.
82-func WithManagePanesLocalPathResolver(resolver port.LocalPathResolver) ManagePanesOption {
83- return func(uc *ManagePanesUseCase) {
84- uc.normalizer = NewNavigationURLNormalizer(resolver)
85- }
86-}
87-
8878 // NewManagePanesUseCase creates a new pane management use case.
89-func NewManagePanesUseCase(idGenerator IDGenerator, opts ...ManagePanesOption) *ManagePanesUseCase {
90- uc := &ManagePanesUseCase{
79+func NewManagePanesUseCase(idGenerator IDGenerator, localPaths port.LocalPathResolver) *ManagePanesUseCase {
80+ return &ManagePanesUseCase{
9181 idGenerator: idGenerator,
92- normalizer: NewNavigationURLNormalizer(nil),
93- }
94- for _, opt := range opts {
95- opt(uc)
82+ normalizer: NewNavigationURLNormalizer(localPaths),
9683 }
97- return uc
9884 }
9985
10086 // NormalizeNavigationURL normalizes user navigation input for pane creation.
@@ -105,6 +91,16 @@ func (uc *ManagePanesUseCase) NormalizeNavigationURL(ctx context.Context, input
10591 return uc.normalizer.normalize(ctx, input)
10692 }
10793
94+func (uc *ManagePanesUseCase) newPane(ctx context.Context, paneID entity.PaneID, initialURL string) *entity.Pane {
95+ pane := entity.NewPane(paneID)
96+ if initialURL != "" {
97+ pane.URI = uc.normalizer.normalize(ctx, initialURL)
98+ } else {
99+ pane.URI = "about:blank"
100+ }
101+ return pane
102+}
103+
108104 // SplitPaneInput contains parameters for splitting a pane.
109105 type SplitPaneInput struct {
110106 Workspace *entity.Workspace
@@ -160,12 +156,7 @@ func (uc *ManagePanesUseCase) Split(ctx context.Context, input SplitPaneInput) (
160156 newPane = input.NewPane
161157 } else {
162158 paneID := entity.PaneID(uc.idGenerator())
163- newPane = entity.NewPane(paneID)
164- if input.InitialURL != "" {
165- newPane.URI = uc.normalizer.normalize(ctx, input.InitialURL)
166- } else {
167- newPane.URI = "about:blank"
168- }
159+ newPane = uc.newPane(ctx, paneID, input.InitialURL)
169160 }
170161
171162 // Create new pane node
@@ -1006,6 +997,7 @@ func (uc *ManagePanesUseCase) CreateStack(
1006997 ctx context.Context,
1007998 ws *entity.Workspace,
1008999 paneNode *entity.PaneNode,
1000+ initialURL string,
10091001 ) (*CreateStackOutput, error) {
10101002 log := logging.FromContext(ctx)
10111003 log.Debug().Str("pane_id", paneNode.ID).Msg("creating stack from pane")
@@ -1036,7 +1028,7 @@ func (uc *ManagePanesUseCase) CreateStack(
10361028
10371029 // Create new pane
10381030 newPaneID := entity.PaneID(uc.idGenerator())
1039- newPane := entity.NewPane(newPaneID)
1031+ newPane := uc.newPane(ctx, newPaneID, initialURL)
10401032
10411033 // Create child node for the new pane
10421034 newChildNode := &entity.PaneNode{
@@ -1081,6 +1073,7 @@ func (uc *ManagePanesUseCase) AddToStack(
10811073 ws *entity.Workspace,
10821074 stackNode *entity.PaneNode,
10831075 pane *entity.Pane,
1076+ initialURL string,
10841077 ) (*AddToStackOutput, error) {
10851078 log := logging.FromContext(ctx)
10861079
@@ -1097,7 +1090,7 @@ func (uc *ManagePanesUseCase) AddToStack(
10971090 // Create pane if not provided
10981091 if pane == nil {
10991092 paneID := entity.PaneID(uc.idGenerator())
1100- pane = entity.NewPane(paneID)
1093+ pane = uc.newPane(ctx, paneID, initialURL)
11011094 }
11021095
11031096 log.Debug().
@@ -75,26 +75,12 @@ type ManagePanesUseCase struct {
75 normalizer *NavigationURLNormalizer75 normalizer *NavigationURLNormalizer
76 }76 }
77 77
78-// ManagePanesOption configures ManagePanesUseCase.
79-type ManagePanesOption func(*ManagePanesUseCase)
80-
81-// WithManagePanesLocalPathResolver configures local path resolution for initial navigation URLs.
82-func WithManagePanesLocalPathResolver(resolver port.LocalPathResolver) ManagePanesOption {
83- return func(uc *ManagePanesUseCase) {
84- uc.normalizer = NewNavigationURLNormalizer(resolver)
85- }
86-}
87-
88 // NewManagePanesUseCase creates a new pane management use case.78 // NewManagePanesUseCase creates a new pane management use case.
89-func NewManagePanesUseCase(idGenerator IDGenerator, opts ...ManagePanesOption) *ManagePanesUseCase {79+func NewManagePanesUseCase(idGenerator IDGenerator, localPaths port.LocalPathResolver) *ManagePanesUseCase {
90- uc := &ManagePanesUseCase{80+ return &ManagePanesUseCase{
91 idGenerator: idGenerator,81 idGenerator: idGenerator,
92- normalizer: NewNavigationURLNormalizer(nil),82+ normalizer: NewNavigationURLNormalizer(localPaths),
93- }
94- for _, opt := range opts {
95- opt(uc)
96 }83 }
97- return uc
98 }84 }
99 85
100 // NormalizeNavigationURL normalizes user navigation input for pane creation.86 // NormalizeNavigationURL normalizes user navigation input for pane creation.
@@ -105,6 +91,16 @@ func (uc *ManagePanesUseCase) NormalizeNavigationURL(ctx context.Context, input
105 return uc.normalizer.normalize(ctx, input)91 return uc.normalizer.normalize(ctx, input)
106 }92 }
107 93
94+func (uc *ManagePanesUseCase) newPane(ctx context.Context, paneID entity.PaneID, initialURL string) *entity.Pane {
95+ pane := entity.NewPane(paneID)
96+ if initialURL != "" {
97+ pane.URI = uc.normalizer.normalize(ctx, initialURL)
98+ } else {
99+ pane.URI = "about:blank"
100+ }
101+ return pane
102+}
103+
108 // SplitPaneInput contains parameters for splitting a pane.104 // SplitPaneInput contains parameters for splitting a pane.
109 type SplitPaneInput struct {105 type SplitPaneInput struct {
110 Workspace *entity.Workspace106 Workspace *entity.Workspace
@@ -160,12 +156,7 @@ func (uc *ManagePanesUseCase) Split(ctx context.Context, input SplitPaneInput) (
160 newPane = input.NewPane156 newPane = input.NewPane
161 } else {157 } else {
162 paneID := entity.PaneID(uc.idGenerator())158 paneID := entity.PaneID(uc.idGenerator())
163- newPane = entity.NewPane(paneID)159+ newPane = uc.newPane(ctx, paneID, input.InitialURL)
164- if input.InitialURL != "" {
165- newPane.URI = uc.normalizer.normalize(ctx, input.InitialURL)
166- } else {
167- newPane.URI = "about:blank"
168- }
169 }160 }
170 161
171 // Create new pane node162 // Create new pane node
@@ -1006,6 +997,7 @@ func (uc *ManagePanesUseCase) CreateStack(
1006 ctx context.Context,997 ctx context.Context,
1007 ws *entity.Workspace,998 ws *entity.Workspace,
1008 paneNode *entity.PaneNode,999 paneNode *entity.PaneNode,
1000+ initialURL string,
1009 ) (*CreateStackOutput, error) {1001 ) (*CreateStackOutput, error) {
1010 log := logging.FromContext(ctx)1002 log := logging.FromContext(ctx)
1011 log.Debug().Str("pane_id", paneNode.ID).Msg("creating stack from pane")1003 log.Debug().Str("pane_id", paneNode.ID).Msg("creating stack from pane")
@@ -1036,7 +1028,7 @@ func (uc *ManagePanesUseCase) CreateStack(
1036 1028
1037 // Create new pane1029 // Create new pane
1038 newPaneID := entity.PaneID(uc.idGenerator())1030 newPaneID := entity.PaneID(uc.idGenerator())
1039- newPane := entity.NewPane(newPaneID)1031+ newPane := uc.newPane(ctx, newPaneID, initialURL)
1040 1032
1041 // Create child node for the new pane1033 // Create child node for the new pane
1042 newChildNode := &entity.PaneNode{1034 newChildNode := &entity.PaneNode{
@@ -1081,6 +1073,7 @@ func (uc *ManagePanesUseCase) AddToStack(
1081 ws *entity.Workspace,1073 ws *entity.Workspace,
1082 stackNode *entity.PaneNode,1074 stackNode *entity.PaneNode,
1083 pane *entity.Pane,1075 pane *entity.Pane,
1076+ initialURL string,
1084 ) (*AddToStackOutput, error) {1077 ) (*AddToStackOutput, error) {
1085 log := logging.FromContext(ctx)1078 log := logging.FromContext(ctx)
1086 1079
@@ -1097,7 +1090,7 @@ func (uc *ManagePanesUseCase) AddToStack(
1097 // Create pane if not provided1090 // Create pane if not provided
1098 if pane == nil {1091 if pane == nil {
1099 paneID := entity.PaneID(uc.idGenerator())1092 paneID := entity.PaneID(uc.idGenerator())
1100- pane = entity.NewPane(paneID)1093+ pane = uc.newPane(ctx, paneID, initialURL)
1101 }1094 }
1102 1095
1103 log.Debug().1096 log.Debug().
M internal/application/usecase/manage_panes_close_stack_test.go +7 -7
@@ -8,7 +8,7 @@ import (
88 )
99
1010 func TestManagePanesUseCase_CloseStackedPane_RemovesFromStackWithMoreThanTwo(t *testing.T) {
11- uc := NewManagePanesUseCase(func() string { return "id" })
11+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
1212 ctx := context.Background()
1313
1414 // Create a stack with 3 panes
@@ -44,7 +44,7 @@ func TestManagePanesUseCase_CloseStackedPane_RemovesFromStackWithMoreThanTwo(t *
4444 }
4545
4646 func TestManagePanesUseCase_CloseStackedPane_DissolvesStackWithTwo(t *testing.T) {
47- uc := NewManagePanesUseCase(func() string { return "id" })
47+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
4848 ctx := context.Background()
4949
5050 // Create a stack with 2 panes
@@ -84,7 +84,7 @@ func TestManagePanesUseCase_CloseStackedPane_DissolvesStackWithTwo(t *testing.T)
8484 }
8585
8686 func TestManagePanesUseCase_CloseStackedPane_UpdatesActivePaneWhenClosingActive(t *testing.T) {
87- uc := NewManagePanesUseCase(func() string { return "id" })
87+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
8888 ctx := context.Background()
8989
9090 // Create a stack with 3 panes, middle one is active
@@ -113,7 +113,7 @@ func TestManagePanesUseCase_CloseStackedPane_UpdatesActivePaneWhenClosingActive(
113113 }
114114
115115 func TestManagePanesUseCase_CloseStackedPane_PreservesActivePaneWhenClosingNonActive(t *testing.T) {
116- uc := NewManagePanesUseCase(func() string { return "id" })
116+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
117117 ctx := context.Background()
118118
119119 // Create a stack with 3 panes, first one is active
@@ -138,7 +138,7 @@ func TestManagePanesUseCase_CloseStackedPane_PreservesActivePaneWhenClosingNonAc
138138 }
139139
140140 func TestManagePanesUseCase_CloseStackedPane_DissolvesNestedStack(t *testing.T) {
141- uc := NewManagePanesUseCase(func() string { return "id" })
141+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
142142 ctx := context.Background()
143143
144144 // Create a split with a stack on the left
@@ -185,7 +185,7 @@ func TestManagePanesUseCase_CloseStackedPane_DissolvesNestedStack(t *testing.T)
185185 }
186186
187187 func TestManagePanesUseCase_CloseStackedPane_ClosingFirstPaneUpdatesIndex(t *testing.T) {
188- uc := NewManagePanesUseCase(func() string { return "id" })
188+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
189189 ctx := context.Background()
190190
191191 // Create a stack with 3 panes, first one is active
@@ -218,7 +218,7 @@ func TestManagePanesUseCase_CloseStackedPane_ClosingFirstPaneUpdatesIndex(t *tes
218218 }
219219
220220 func TestManagePanesUseCase_CloseStackedPane_ClosingLastPaneUpdatesIndex(t *testing.T) {
221- uc := NewManagePanesUseCase(func() string { return "id" })
221+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
222222 ctx := context.Background()
223223
224224 // Create a stack with 3 panes, last one is active
@@ -8,7 +8,7 @@ import (
8 )8 )
9 9
10 func TestManagePanesUseCase_CloseStackedPane_RemovesFromStackWithMoreThanTwo(t *testing.T) {10 func TestManagePanesUseCase_CloseStackedPane_RemovesFromStackWithMoreThanTwo(t *testing.T) {
11- uc := NewManagePanesUseCase(func() string { return "id" })11+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
12 ctx := context.Background()12 ctx := context.Background()
13 13
14 // Create a stack with 3 panes14 // Create a stack with 3 panes
@@ -44,7 +44,7 @@ func TestManagePanesUseCase_CloseStackedPane_RemovesFromStackWithMoreThanTwo(t *
44 }44 }
45 45
46 func TestManagePanesUseCase_CloseStackedPane_DissolvesStackWithTwo(t *testing.T) {46 func TestManagePanesUseCase_CloseStackedPane_DissolvesStackWithTwo(t *testing.T) {
47- uc := NewManagePanesUseCase(func() string { return "id" })47+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
48 ctx := context.Background()48 ctx := context.Background()
49 49
50 // Create a stack with 2 panes50 // Create a stack with 2 panes
@@ -84,7 +84,7 @@ func TestManagePanesUseCase_CloseStackedPane_DissolvesStackWithTwo(t *testing.T)
84 }84 }
85 85
86 func TestManagePanesUseCase_CloseStackedPane_UpdatesActivePaneWhenClosingActive(t *testing.T) {86 func TestManagePanesUseCase_CloseStackedPane_UpdatesActivePaneWhenClosingActive(t *testing.T) {
87- uc := NewManagePanesUseCase(func() string { return "id" })87+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
88 ctx := context.Background()88 ctx := context.Background()
89 89
90 // Create a stack with 3 panes, middle one is active90 // Create a stack with 3 panes, middle one is active
@@ -113,7 +113,7 @@ func TestManagePanesUseCase_CloseStackedPane_UpdatesActivePaneWhenClosingActive(
113 }113 }
114 114
115 func TestManagePanesUseCase_CloseStackedPane_PreservesActivePaneWhenClosingNonActive(t *testing.T) {115 func TestManagePanesUseCase_CloseStackedPane_PreservesActivePaneWhenClosingNonActive(t *testing.T) {
116- uc := NewManagePanesUseCase(func() string { return "id" })116+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
117 ctx := context.Background()117 ctx := context.Background()
118 118
119 // Create a stack with 3 panes, first one is active119 // Create a stack with 3 panes, first one is active
@@ -138,7 +138,7 @@ func TestManagePanesUseCase_CloseStackedPane_PreservesActivePaneWhenClosingNonAc
138 }138 }
139 139
140 func TestManagePanesUseCase_CloseStackedPane_DissolvesNestedStack(t *testing.T) {140 func TestManagePanesUseCase_CloseStackedPane_DissolvesNestedStack(t *testing.T) {
141- uc := NewManagePanesUseCase(func() string { return "id" })141+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
142 ctx := context.Background()142 ctx := context.Background()
143 143
144 // Create a split with a stack on the left144 // Create a split with a stack on the left
@@ -185,7 +185,7 @@ func TestManagePanesUseCase_CloseStackedPane_DissolvesNestedStack(t *testing.T)
185 }185 }
186 186
187 func TestManagePanesUseCase_CloseStackedPane_ClosingFirstPaneUpdatesIndex(t *testing.T) {187 func TestManagePanesUseCase_CloseStackedPane_ClosingFirstPaneUpdatesIndex(t *testing.T) {
188- uc := NewManagePanesUseCase(func() string { return "id" })188+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
189 ctx := context.Background()189 ctx := context.Background()
190 190
191 // Create a stack with 3 panes, first one is active191 // Create a stack with 3 panes, first one is active
@@ -218,7 +218,7 @@ func TestManagePanesUseCase_CloseStackedPane_ClosingFirstPaneUpdatesIndex(t *tes
218 }218 }
219 219
220 func TestManagePanesUseCase_CloseStackedPane_ClosingLastPaneUpdatesIndex(t *testing.T) {220 func TestManagePanesUseCase_CloseStackedPane_ClosingLastPaneUpdatesIndex(t *testing.T) {
221- uc := NewManagePanesUseCase(func() string { return "id" })221+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
222 ctx := context.Background()222 ctx := context.Background()
223 223
224 // Create a stack with 3 panes, last one is active224 // Create a stack with 3 panes, last one is active
M internal/application/usecase/manage_panes_consume_expel_test.go +11 -11
@@ -8,7 +8,7 @@ import (
88 )
99
1010 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_CyclesHorizontalToVerticalThenStack(t *testing.T) {
11- uc := NewManagePanesUseCase(func() string { return "id" })
11+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
1212 ctx := context.Background()
1313
1414 left := leaf("left")
@@ -61,7 +61,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_CyclesHorizontalToVertica
6161 }
6262
6363 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeRight_CyclesHorizontalToVerticalThenStack(t *testing.T) {
64- uc := NewManagePanesUseCase(func() string { return "id" })
64+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
6565 ctx := context.Background()
6666
6767 left := leaf("left")
@@ -114,7 +114,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeRight_CyclesHorizontalToVertic
114114 }
115115
116116 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeIntoExistingStack_AppendsToEnd(t *testing.T) {
117- uc := NewManagePanesUseCase(func() string { return "id" })
117+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
118118 ctx := context.Background()
119119
120120 stackNode := stack(leaf("a"), leaf("b"))
@@ -141,7 +141,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeIntoExistingStack_AppendsToEnd
141141 }
142142
143143 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_InVerticalSplitWithStackedSibling_Noops(t *testing.T) {
144- uc := NewManagePanesUseCase(func() string { return "id" })
144+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
145145 ctx := context.Background()
146146
147147 top := leaf("top")
@@ -165,7 +165,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_InVerticalSplitWithStacke
165165 }
166166
167167 func TestManagePanesUseCase_ConsumeOrExpel_Expel_DissolvesTwoPaneStack(t *testing.T) {
168- uc := NewManagePanesUseCase(func() string { return "id" })
168+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
169169 ctx := context.Background()
170170
171171 stackNode := stack(leaf("a"), leaf("b"))
@@ -200,7 +200,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_Expel_DissolvesTwoPaneStack(t *testin
200200 }
201201
202202 func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeRight_CyclesVerticalToHorizontal(t *testing.T) {
203- uc := NewManagePanesUseCase(func() string { return "id" })
203+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
204204 ctx := context.Background()
205205
206206 stackNode := stack(leaf("a"), leaf("b"))
@@ -228,7 +228,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeRight_CyclesVerticalT
228228 }
229229
230230 func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeLeft_ReturnsToStack(t *testing.T) {
231- uc := NewManagePanesUseCase(func() string { return "id" })
231+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
232232 ctx := context.Background()
233233
234234 stackNode := stack(leaf("a"), leaf("b"))
@@ -259,7 +259,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeLeft_ReturnsToStack(t
259259 }
260260
261261 func TestManagePanesUseCase_ConsumeOrExpel_Expel_StackRemainsWhenMoreThanTwo(t *testing.T) {
262- uc := NewManagePanesUseCase(func() string { return "id" })
262+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
263263 ctx := context.Background()
264264
265265 stackNode := stack(leaf("a"), leaf("b"), leaf("c"))
@@ -293,7 +293,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_Expel_StackRemainsWhenMoreThanTwo(t *
293293 }
294294
295295 func TestManagePanesUseCase_ConsumeOrExpel_NoSibling_ReturnsWarningAndNoop(t *testing.T) {
296- uc := NewManagePanesUseCase(func() string { return "id" })
296+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
297297 ctx := context.Background()
298298
299299 left := leaf("left")
@@ -317,7 +317,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_NoSibling_ReturnsWarningAndNoop(t *te
317317 }
318318
319319 func TestManagePanesUseCase_ConsumeOrExpel_NestedSplits_ConsumesImmediateSiblingOnly(t *testing.T) {
320- uc := NewManagePanesUseCase(func() string { return "id" })
320+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
321321 ctx := context.Background()
322322
323323 a := leaf("a")
@@ -346,7 +346,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_NestedSplits_ConsumesImmediateSibling
346346 }
347347
348348 func TestManagePanesUseCase_ConsumeOrExpel_OnlyOnePane(t *testing.T) {
349- uc := NewManagePanesUseCase(func() string { return "id" })
349+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
350350 ctx := context.Background()
351351
352352 only := leaf("only")
@@ -8,7 +8,7 @@ import (
8 )8 )
9 9
10 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_CyclesHorizontalToVerticalThenStack(t *testing.T) {10 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_CyclesHorizontalToVerticalThenStack(t *testing.T) {
11- uc := NewManagePanesUseCase(func() string { return "id" })11+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
12 ctx := context.Background()12 ctx := context.Background()
13 13
14 left := leaf("left")14 left := leaf("left")
@@ -61,7 +61,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_CyclesHorizontalToVertica
61 }61 }
62 62
63 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeRight_CyclesHorizontalToVerticalThenStack(t *testing.T) {63 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeRight_CyclesHorizontalToVerticalThenStack(t *testing.T) {
64- uc := NewManagePanesUseCase(func() string { return "id" })64+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
65 ctx := context.Background()65 ctx := context.Background()
66 66
67 left := leaf("left")67 left := leaf("left")
@@ -114,7 +114,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeRight_CyclesHorizontalToVertic
114 }114 }
115 115
116 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeIntoExistingStack_AppendsToEnd(t *testing.T) {116 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeIntoExistingStack_AppendsToEnd(t *testing.T) {
117- uc := NewManagePanesUseCase(func() string { return "id" })117+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
118 ctx := context.Background()118 ctx := context.Background()
119 119
120 stackNode := stack(leaf("a"), leaf("b"))120 stackNode := stack(leaf("a"), leaf("b"))
@@ -141,7 +141,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeIntoExistingStack_AppendsToEnd
141 }141 }
142 142
143 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_InVerticalSplitWithStackedSibling_Noops(t *testing.T) {143 func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_InVerticalSplitWithStackedSibling_Noops(t *testing.T) {
144- uc := NewManagePanesUseCase(func() string { return "id" })144+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
145 ctx := context.Background()145 ctx := context.Background()
146 146
147 top := leaf("top")147 top := leaf("top")
@@ -165,7 +165,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ConsumeLeft_InVerticalSplitWithStacke
165 }165 }
166 166
167 func TestManagePanesUseCase_ConsumeOrExpel_Expel_DissolvesTwoPaneStack(t *testing.T) {167 func TestManagePanesUseCase_ConsumeOrExpel_Expel_DissolvesTwoPaneStack(t *testing.T) {
168- uc := NewManagePanesUseCase(func() string { return "id" })168+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
169 ctx := context.Background()169 ctx := context.Background()
170 170
171 stackNode := stack(leaf("a"), leaf("b"))171 stackNode := stack(leaf("a"), leaf("b"))
@@ -200,7 +200,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_Expel_DissolvesTwoPaneStack(t *testin
200 }200 }
201 201
202 func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeRight_CyclesVerticalToHorizontal(t *testing.T) {202 func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeRight_CyclesVerticalToHorizontal(t *testing.T) {
203- uc := NewManagePanesUseCase(func() string { return "id" })203+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
204 ctx := context.Background()204 ctx := context.Background()
205 205
206 stackNode := stack(leaf("a"), leaf("b"))206 stackNode := stack(leaf("a"), leaf("b"))
@@ -228,7 +228,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeRight_CyclesVerticalT
228 }228 }
229 229
230 func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeLeft_ReturnsToStack(t *testing.T) {230 func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeLeft_ReturnsToStack(t *testing.T) {
231- uc := NewManagePanesUseCase(func() string { return "id" })231+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
232 ctx := context.Background()232 ctx := context.Background()
233 233
234 stackNode := stack(leaf("a"), leaf("b"))234 stackNode := stack(leaf("a"), leaf("b"))
@@ -259,7 +259,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_ExpelThenConsumeLeft_ReturnsToStack(t
259 }259 }
260 260
261 func TestManagePanesUseCase_ConsumeOrExpel_Expel_StackRemainsWhenMoreThanTwo(t *testing.T) {261 func TestManagePanesUseCase_ConsumeOrExpel_Expel_StackRemainsWhenMoreThanTwo(t *testing.T) {
262- uc := NewManagePanesUseCase(func() string { return "id" })262+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
263 ctx := context.Background()263 ctx := context.Background()
264 264
265 stackNode := stack(leaf("a"), leaf("b"), leaf("c"))265 stackNode := stack(leaf("a"), leaf("b"), leaf("c"))
@@ -293,7 +293,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_Expel_StackRemainsWhenMoreThanTwo(t *
293 }293 }
294 294
295 func TestManagePanesUseCase_ConsumeOrExpel_NoSibling_ReturnsWarningAndNoop(t *testing.T) {295 func TestManagePanesUseCase_ConsumeOrExpel_NoSibling_ReturnsWarningAndNoop(t *testing.T) {
296- uc := NewManagePanesUseCase(func() string { return "id" })296+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
297 ctx := context.Background()297 ctx := context.Background()
298 298
299 left := leaf("left")299 left := leaf("left")
@@ -317,7 +317,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_NoSibling_ReturnsWarningAndNoop(t *te
317 }317 }
318 318
319 func TestManagePanesUseCase_ConsumeOrExpel_NestedSplits_ConsumesImmediateSiblingOnly(t *testing.T) {319 func TestManagePanesUseCase_ConsumeOrExpel_NestedSplits_ConsumesImmediateSiblingOnly(t *testing.T) {
320- uc := NewManagePanesUseCase(func() string { return "id" })320+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
321 ctx := context.Background()321 ctx := context.Background()
322 322
323 a := leaf("a")323 a := leaf("a")
@@ -346,7 +346,7 @@ func TestManagePanesUseCase_ConsumeOrExpel_NestedSplits_ConsumesImmediateSibling
346 }346 }
347 347
348 func TestManagePanesUseCase_ConsumeOrExpel_OnlyOnePane(t *testing.T) {348 func TestManagePanesUseCase_ConsumeOrExpel_OnlyOnePane(t *testing.T) {
349- uc := NewManagePanesUseCase(func() string { return "id" })349+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
350 ctx := context.Background()350 ctx := context.Background()
351 351
352 only := leaf("only")352 only := leaf("only")
M internal/application/usecase/manage_panes_resize_test.go +5 -5
@@ -9,7 +9,7 @@ import (
99 )
1010
1111 func TestManagePanesUseCase_Resize_Errors(t *testing.T) {
12- uc := NewManagePanesUseCase(func() string { return "id" })
12+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
1313
1414 ctx := context.Background()
1515
@@ -33,7 +33,7 @@ func TestManagePanesUseCase_Resize_Errors(t *testing.T) {
3333 }
3434
3535 func TestManagePanesUseCase_Resize_VerticalDividerMove(t *testing.T) {
36- uc := NewManagePanesUseCase(func() string { return "id" })
36+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
3737 ctx := context.Background()
3838
3939 top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
@@ -67,7 +67,7 @@ func TestManagePanesUseCase_Resize_VerticalDividerMove(t *testing.T) {
6767 }
6868
6969 func TestManagePanesUseCase_Resize_SmartResizeGrowsAndShrinksActivePane(t *testing.T) {
70- uc := NewManagePanesUseCase(func() string { return "id" })
70+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
7171 ctx := context.Background()
7272
7373 top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
@@ -101,7 +101,7 @@ func TestManagePanesUseCase_Resize_SmartResizeGrowsAndShrinksActivePane(t *testi
101101 }
102102
103103 func TestManagePanesUseCase_Resize_ClampsToMinPanePercent(t *testing.T) {
104- uc := NewManagePanesUseCase(func() string { return "id" })
104+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
105105 ctx := context.Background()
106106
107107 left := &entity.PaneNode{ID: "left", Pane: &entity.Pane{ID: "left"}}
@@ -127,7 +127,7 @@ func TestManagePanesUseCase_Resize_ClampsToMinPanePercent(t *testing.T) {
127127 }
128128
129129 func TestManagePanesUseCase_Resize_TargetsStackContainer(t *testing.T) {
130- uc := NewManagePanesUseCase(func() string { return "id" })
130+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
131131 ctx := context.Background()
132132
133133 stackLeaf1 := &entity.PaneNode{ID: "s1", Pane: &entity.Pane{ID: "s1"}}
@@ -9,7 +9,7 @@ import (
9 )9 )
10 10
11 func TestManagePanesUseCase_Resize_Errors(t *testing.T) {11 func TestManagePanesUseCase_Resize_Errors(t *testing.T) {
12- uc := NewManagePanesUseCase(func() string { return "id" })12+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
13 13
14 ctx := context.Background()14 ctx := context.Background()
15 15
@@ -33,7 +33,7 @@ func TestManagePanesUseCase_Resize_Errors(t *testing.T) {
33 }33 }
34 34
35 func TestManagePanesUseCase_Resize_VerticalDividerMove(t *testing.T) {35 func TestManagePanesUseCase_Resize_VerticalDividerMove(t *testing.T) {
36- uc := NewManagePanesUseCase(func() string { return "id" })36+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
37 ctx := context.Background()37 ctx := context.Background()
38 38
39 top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}39 top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
@@ -67,7 +67,7 @@ func TestManagePanesUseCase_Resize_VerticalDividerMove(t *testing.T) {
67 }67 }
68 68
69 func TestManagePanesUseCase_Resize_SmartResizeGrowsAndShrinksActivePane(t *testing.T) {69 func TestManagePanesUseCase_Resize_SmartResizeGrowsAndShrinksActivePane(t *testing.T) {
70- uc := NewManagePanesUseCase(func() string { return "id" })70+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
71 ctx := context.Background()71 ctx := context.Background()
72 72
73 top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}73 top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
@@ -101,7 +101,7 @@ func TestManagePanesUseCase_Resize_SmartResizeGrowsAndShrinksActivePane(t *testi
101 }101 }
102 102
103 func TestManagePanesUseCase_Resize_ClampsToMinPanePercent(t *testing.T) {103 func TestManagePanesUseCase_Resize_ClampsToMinPanePercent(t *testing.T) {
104- uc := NewManagePanesUseCase(func() string { return "id" })104+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
105 ctx := context.Background()105 ctx := context.Background()
106 106
107 left := &entity.PaneNode{ID: "left", Pane: &entity.Pane{ID: "left"}}107 left := &entity.PaneNode{ID: "left", Pane: &entity.Pane{ID: "left"}}
@@ -127,7 +127,7 @@ func TestManagePanesUseCase_Resize_ClampsToMinPanePercent(t *testing.T) {
127 }127 }
128 128
129 func TestManagePanesUseCase_Resize_TargetsStackContainer(t *testing.T) {129 func TestManagePanesUseCase_Resize_TargetsStackContainer(t *testing.T) {
130- uc := NewManagePanesUseCase(func() string { return "id" })130+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
131 ctx := context.Background()131 ctx := context.Background()
132 132
133 stackLeaf1 := &entity.PaneNode{ID: "s1", Pane: &entity.Pane{ID: "s1"}}133 stackLeaf1 := &entity.PaneNode{ID: "s1", Pane: &entity.Pane{ID: "s1"}}
M internal/application/usecase/manage_panes_split_test.go +2 -2
@@ -9,7 +9,7 @@ import (
99 )
1010
1111 func TestManagePanesUseCase_Split_ValidDirections(t *testing.T) {
12- uc := NewManagePanesUseCase(func() string { return "id" })
12+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
1313 ctx := context.Background()
1414
1515 dirs := []SplitDirection{SplitLeft, SplitRight, SplitUp, SplitDown}
@@ -51,7 +51,7 @@ func TestManagePanesUseCase_Split_ValidDirections(t *testing.T) {
5151 }
5252
5353 func TestManagePanesUseCase_Split_InvalidDirection_ReturnsError(t *testing.T) {
54- uc := NewManagePanesUseCase(func() string { return "id" })
54+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
5555 ctx := context.Background()
5656
5757 target := leaf("target")
@@ -9,7 +9,7 @@ import (
9 )9 )
10 10
11 func TestManagePanesUseCase_Split_ValidDirections(t *testing.T) {11 func TestManagePanesUseCase_Split_ValidDirections(t *testing.T) {
12- uc := NewManagePanesUseCase(func() string { return "id" })12+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
13 ctx := context.Background()13 ctx := context.Background()
14 14
15 dirs := []SplitDirection{SplitLeft, SplitRight, SplitUp, SplitDown}15 dirs := []SplitDirection{SplitLeft, SplitRight, SplitUp, SplitDown}
@@ -51,7 +51,7 @@ func TestManagePanesUseCase_Split_ValidDirections(t *testing.T) {
51 }51 }
52 52
53 func TestManagePanesUseCase_Split_InvalidDirection_ReturnsError(t *testing.T) {53 func TestManagePanesUseCase_Split_InvalidDirection_ReturnsError(t *testing.T) {
54- uc := NewManagePanesUseCase(func() string { return "id" })54+ uc := NewManagePanesUseCase(func() string { return "id" }, nil)
55 ctx := context.Background()55 ctx := context.Background()
56 56
57 target := leaf("target")57 target := leaf("target")
M internal/application/usecase/manage_tabs.go +3 -17
@@ -18,26 +18,12 @@ type ManageTabsUseCase struct {
1818 normalizer *NavigationURLNormalizer
1919 }
2020
21-// ManageTabsOption configures ManageTabsUseCase.
22-type ManageTabsOption func(*ManageTabsUseCase)
23-
24-// WithManageTabsLocalPathResolver configures local path resolution for initial navigation URLs.
25-func WithManageTabsLocalPathResolver(resolver port.LocalPathResolver) ManageTabsOption {
26- return func(uc *ManageTabsUseCase) {
27- uc.normalizer = NewNavigationURLNormalizer(resolver)
28- }
29-}
30-
3121 // NewManageTabsUseCase creates a new tab management use case.
32-func NewManageTabsUseCase(idGenerator IDGenerator, opts ...ManageTabsOption) *ManageTabsUseCase {
33- uc := &ManageTabsUseCase{
22+func NewManageTabsUseCase(idGenerator IDGenerator, localPaths port.LocalPathResolver) *ManageTabsUseCase {
23+ return &ManageTabsUseCase{
3424 idGenerator: idGenerator,
35- normalizer: NewNavigationURLNormalizer(nil),
36- }
37- for _, opt := range opts {
38- opt(uc)
25+ normalizer: NewNavigationURLNormalizer(localPaths),
3926 }
40- return uc
4127 }
4228
4329 // CreateTabInput contains parameters for creating a new tab.
@@ -18,26 +18,12 @@ type ManageTabsUseCase struct {
18 normalizer *NavigationURLNormalizer18 normalizer *NavigationURLNormalizer
19 }19 }
20 20
21-// ManageTabsOption configures ManageTabsUseCase.
22-type ManageTabsOption func(*ManageTabsUseCase)
23-
24-// WithManageTabsLocalPathResolver configures local path resolution for initial navigation URLs.
25-func WithManageTabsLocalPathResolver(resolver port.LocalPathResolver) ManageTabsOption {
26- return func(uc *ManageTabsUseCase) {
27- uc.normalizer = NewNavigationURLNormalizer(resolver)
28- }
29-}
30-
31 // NewManageTabsUseCase creates a new tab management use case.21 // NewManageTabsUseCase creates a new tab management use case.
32-func NewManageTabsUseCase(idGenerator IDGenerator, opts ...ManageTabsOption) *ManageTabsUseCase {22+func NewManageTabsUseCase(idGenerator IDGenerator, localPaths port.LocalPathResolver) *ManageTabsUseCase {
33- uc := &ManageTabsUseCase{23+ return &ManageTabsUseCase{
34 idGenerator: idGenerator,24 idGenerator: idGenerator,
35- normalizer: NewNavigationURLNormalizer(nil),25+ normalizer: NewNavigationURLNormalizer(localPaths),
36- }
37- for _, opt := range opts {
38- opt(uc)
39 }26 }
40- return uc
41 }27 }
42 28
43 // CreateTabInput contains parameters for creating a new tab.29 // CreateTabInput contains parameters for creating a new tab.
M internal/application/usecase/navigation_url_usecase_integration_test.go +3 -3
@@ -14,7 +14,7 @@ func TestManageTabsCreateResolvesExistingLocalInitialURL(t *testing.T) {
1414 tabsFile := filepath.Join(string(filepath.Separator), "tmp", "tabs.html")
1515 uc := NewManageTabsUseCase(
1616 sequentialIDGenerator("tab", "workspace", "pane"),
17- WithManageTabsLocalPathResolver(fakeLocalPathResolver{paths: map[string]string{"tabs.html": tabsFile}}),
17+ fakeLocalPathResolver{paths: map[string]string{"tabs.html": tabsFile}},
1818 )
1919
2020 out, err := uc.Create(ctx, CreateTabInput{TabList: tabs, InitialURL: "tabs.html"})
@@ -32,7 +32,7 @@ func TestManageTabsCreateWithPaneResolvesExistingLocalInitialURL(t *testing.T) {
3232 tabsFile := filepath.Join(string(filepath.Separator), "tmp", "provided.html")
3333 uc := NewManageTabsUseCase(
3434 sequentialIDGenerator("tab", "workspace"),
35- WithManageTabsLocalPathResolver(fakeLocalPathResolver{paths: map[string]string{"provided.html": tabsFile}}),
35+ fakeLocalPathResolver{paths: map[string]string{"provided.html": tabsFile}},
3636 )
3737 pane := entity.NewPane("provided-pane")
3838
@@ -50,7 +50,7 @@ func TestManagePanesSplitResolvesExistingLocalInitialURL(t *testing.T) {
5050 paneFile := filepath.Join(string(filepath.Separator), "tmp", "split.html")
5151 uc := NewManagePanesUseCase(
5252 sequentialIDGenerator("new-pane", "parent"),
53- WithManagePanesLocalPathResolver(fakeLocalPathResolver{paths: map[string]string{"split.html": paneFile}}),
53+ fakeLocalPathResolver{paths: map[string]string{"split.html": paneFile}},
5454 )
5555 active := entity.NewPane("active-pane")
5656 activeNode := &entity.PaneNode{ID: "active-node", Pane: active}
@@ -14,7 +14,7 @@ func TestManageTabsCreateResolvesExistingLocalInitialURL(t *testing.T) {
14 tabsFile := filepath.Join(string(filepath.Separator), "tmp", "tabs.html")14 tabsFile := filepath.Join(string(filepath.Separator), "tmp", "tabs.html")
15 uc := NewManageTabsUseCase(15 uc := NewManageTabsUseCase(
16 sequentialIDGenerator("tab", "workspace", "pane"),16 sequentialIDGenerator("tab", "workspace", "pane"),
17- WithManageTabsLocalPathResolver(fakeLocalPathResolver{paths: map[string]string{"tabs.html": tabsFile}}),17+ fakeLocalPathResolver{paths: map[string]string{"tabs.html": tabsFile}},
18 )18 )
19 19
20 out, err := uc.Create(ctx, CreateTabInput{TabList: tabs, InitialURL: "tabs.html"})20 out, err := uc.Create(ctx, CreateTabInput{TabList: tabs, InitialURL: "tabs.html"})
@@ -32,7 +32,7 @@ func TestManageTabsCreateWithPaneResolvesExistingLocalInitialURL(t *testing.T) {
32 tabsFile := filepath.Join(string(filepath.Separator), "tmp", "provided.html")32 tabsFile := filepath.Join(string(filepath.Separator), "tmp", "provided.html")
33 uc := NewManageTabsUseCase(33 uc := NewManageTabsUseCase(
34 sequentialIDGenerator("tab", "workspace"),34 sequentialIDGenerator("tab", "workspace"),
35- WithManageTabsLocalPathResolver(fakeLocalPathResolver{paths: map[string]string{"provided.html": tabsFile}}),35+ fakeLocalPathResolver{paths: map[string]string{"provided.html": tabsFile}},
36 )36 )
37 pane := entity.NewPane("provided-pane")37 pane := entity.NewPane("provided-pane")
38 38
@@ -50,7 +50,7 @@ func TestManagePanesSplitResolvesExistingLocalInitialURL(t *testing.T) {
50 paneFile := filepath.Join(string(filepath.Separator), "tmp", "split.html")50 paneFile := filepath.Join(string(filepath.Separator), "tmp", "split.html")
51 uc := NewManagePanesUseCase(51 uc := NewManagePanesUseCase(
52 sequentialIDGenerator("new-pane", "parent"),52 sequentialIDGenerator("new-pane", "parent"),
53- WithManagePanesLocalPathResolver(fakeLocalPathResolver{paths: map[string]string{"split.html": paneFile}}),53+ fakeLocalPathResolver{paths: map[string]string{"split.html": paneFile}},
54 )54 )
55 active := entity.NewPane("active-pane")55 active := entity.NewPane("active-pane")
56 activeNode := &entity.PaneNode{ID: "active-node", Pane: active}56 activeNode := &entity.PaneNode{ID: "active-node", Pane: active}
M internal/infrastructure/config/validation.go +25 -4
@@ -446,7 +446,13 @@ func validateWorkspaceNewPaneURL(config *Config) []string {
446446
447447 func validateWorkspaceURLValue(fieldPath, value string) []string {
448448 trimmed := strings.TrimSpace(value)
449- if isLocalPathLikeWorkspaceURL(trimmed) || isExistingRelativeWorkspacePath(trimmed) {
449+ if isLocalPathLikeWorkspaceURL(trimmed) {
450+ if isExistingWorkspacePath(trimmed) {
451+ return nil
452+ }
453+ return []string{fmt.Sprintf("%s local path must exist (got: %s)", fieldPath, value)}
454+ }
455+ if isExistingRelativeWorkspacePath(trimmed) {
450456 return nil
451457 }
452458 candidate := trimmed
@@ -478,11 +484,19 @@ func isLocalPathLikeWorkspaceURL(value string) bool {
478484 strings.HasPrefix(value, "~/")
479485 }
480486
481-func isExistingRelativeWorkspacePath(value string) bool {
482- if value == "" || strings.Contains(value, "://") || filepath.IsAbs(value) {
487+func isExistingWorkspacePath(value string) bool {
488+ if value == "" || strings.Contains(value, "://") {
483489 return false
484490 }
485- absPath, err := filepath.Abs(value)
491+ candidate := value
492+ if strings.HasPrefix(candidate, "~/") {
493+ homeDir, err := os.UserHomeDir()
494+ if err != nil || homeDir == "" {
495+ return false
496+ }
497+ candidate = filepath.Join(homeDir, strings.TrimPrefix(candidate, "~/"))
498+ }
499+ absPath, err := filepath.Abs(candidate)
486500 if err != nil {
487501 return false
488502 }
@@ -490,6 +504,13 @@ func isExistingRelativeWorkspacePath(value string) bool {
490504 return err == nil
491505 }
492506
507+func isExistingRelativeWorkspacePath(value string) bool {
508+ if value == "" || strings.Contains(value, "://") || filepath.IsAbs(value) {
509+ return false
510+ }
511+ return isExistingWorkspacePath(value)
512+}
513+
493514 func validateOmnibox(config *Config) []string {
494515 var validationErrors []string
495516 if config.Omnibox.MostVisitedDays < 0 {
@@ -446,7 +446,13 @@ func validateWorkspaceNewPaneURL(config *Config) []string {
446 446
447 func validateWorkspaceURLValue(fieldPath, value string) []string {447 func validateWorkspaceURLValue(fieldPath, value string) []string {
448 trimmed := strings.TrimSpace(value)448 trimmed := strings.TrimSpace(value)
449- if isLocalPathLikeWorkspaceURL(trimmed) || isExistingRelativeWorkspacePath(trimmed) {449+ if isLocalPathLikeWorkspaceURL(trimmed) {
450+ if isExistingWorkspacePath(trimmed) {
451+ return nil
452+ }
453+ return []string{fmt.Sprintf("%s local path must exist (got: %s)", fieldPath, value)}
454+ }
455+ if isExistingRelativeWorkspacePath(trimmed) {
450 return nil456 return nil
451 }457 }
452 candidate := trimmed458 candidate := trimmed
@@ -478,11 +484,19 @@ func isLocalPathLikeWorkspaceURL(value string) bool {
478 strings.HasPrefix(value, "~/")484 strings.HasPrefix(value, "~/")
479 }485 }
480 486
481-func isExistingRelativeWorkspacePath(value string) bool {487+func isExistingWorkspacePath(value string) bool {
482- if value == "" || strings.Contains(value, "://") || filepath.IsAbs(value) {488+ if value == "" || strings.Contains(value, "://") {
483 return false489 return false
484 }490 }
485- absPath, err := filepath.Abs(value)491+ candidate := value
492+ if strings.HasPrefix(candidate, "~/") {
493+ homeDir, err := os.UserHomeDir()
494+ if err != nil || homeDir == "" {
495+ return false
496+ }
497+ candidate = filepath.Join(homeDir, strings.TrimPrefix(candidate, "~/"))
498+ }
499+ absPath, err := filepath.Abs(candidate)
486 if err != nil {500 if err != nil {
487 return false501 return false
488 }502 }
@@ -490,6 +504,13 @@ func isExistingRelativeWorkspacePath(value string) bool {
490 return err == nil504 return err == nil
491 }505 }
492 506
507+func isExistingRelativeWorkspacePath(value string) bool {
508+ if value == "" || strings.Contains(value, "://") || filepath.IsAbs(value) {
509+ return false
510+ }
511+ return isExistingWorkspacePath(value)
512+}
513+
493 func validateOmnibox(config *Config) []string {514 func validateOmnibox(config *Config) []string {
494 var validationErrors []string515 var validationErrors []string
495 if config.Omnibox.MostVisitedDays < 0 {516 if config.Omnibox.MostVisitedDays < 0 {
M internal/infrastructure/config/validation_test.go +26 -2
@@ -3,6 +3,7 @@ package config
33 import (
44 "math"
55 "os"
6+ "path/filepath"
67 "testing"
78
89 "github.com/stretchr/testify/assert"
@@ -293,8 +294,18 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
293294 }
294295 }
295296
296-func TestValidateConfig_WorkspaceNewPaneURLAllowsLocalPathLikeValues(t *testing.T) {
297- for _, value := range []string{"/tmp/page.html", "./page.html", "../page.html", "~/page.html"} {
297+func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingLocalPathLikeValues(t *testing.T) {
298+ tmpDir := t.TempDir()
299+ oldWD, err := os.Getwd()
300+ require.NoError(t, err)
301+ childDir := filepath.Join(tmpDir, "child")
302+ require.NoError(t, os.Mkdir(childDir, 0755))
303+ require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "page.html"), []byte("ok"), 0644))
304+ require.NoError(t, os.WriteFile(filepath.Join(childDir, "page.html"), []byte("ok"), 0644))
305+ require.NoError(t, os.Chdir(childDir))
306+ t.Cleanup(func() { _ = os.Chdir(oldWD) })
307+
308+ for _, value := range []string{filepath.Join(tmpDir, "page.html"), "./page.html", "../page.html"} {
298309 t.Run(value, func(t *testing.T) {
299310 cfg := DefaultConfig()
300311 cfg.Workspace.NewPaneURL = value
@@ -305,6 +316,19 @@ func TestValidateConfig_WorkspaceNewPaneURLAllowsLocalPathLikeValues(t *testing.
305316 }
306317 }
307318
319+func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *testing.T) {
320+ for _, value := range []string{"/missing/dumber/page.html", "./missing.html", "../missing.html", "~/missing-dumber-page.html"} {
321+ t.Run(value, func(t *testing.T) {
322+ cfg := DefaultConfig()
323+ cfg.Workspace.NewPaneURL = value
324+
325+ err := validateConfig(cfg)
326+ require.Error(t, err)
327+ assert.Contains(t, err.Error(), "workspace.new_pane_url local path must exist")
328+ })
329+ }
330+}
331+
308332 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingBareRelativeFile(t *testing.T) {
309333 tmpDir := t.TempDir()
310334 oldWD, err := os.Getwd()
@@ -3,6 +3,7 @@ package config
3 import (3 import (
4 "math"4 "math"
5 "os"5 "os"
6+ "path/filepath"
6 "testing"7 "testing"
7 8
8 "github.com/stretchr/testify/assert"9 "github.com/stretchr/testify/assert"
@@ -293,8 +294,18 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
293 }294 }
294 }295 }
295 296
296-func TestValidateConfig_WorkspaceNewPaneURLAllowsLocalPathLikeValues(t *testing.T) {297+func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingLocalPathLikeValues(t *testing.T) {
297- for _, value := range []string{"/tmp/page.html", "./page.html", "../page.html", "~/page.html"} {298+ tmpDir := t.TempDir()
299+ oldWD, err := os.Getwd()
300+ require.NoError(t, err)
301+ childDir := filepath.Join(tmpDir, "child")
302+ require.NoError(t, os.Mkdir(childDir, 0755))
303+ require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "page.html"), []byte("ok"), 0644))
304+ require.NoError(t, os.WriteFile(filepath.Join(childDir, "page.html"), []byte("ok"), 0644))
305+ require.NoError(t, os.Chdir(childDir))
306+ t.Cleanup(func() { _ = os.Chdir(oldWD) })
307+
308+ for _, value := range []string{filepath.Join(tmpDir, "page.html"), "./page.html", "../page.html"} {
298 t.Run(value, func(t *testing.T) {309 t.Run(value, func(t *testing.T) {
299 cfg := DefaultConfig()310 cfg := DefaultConfig()
300 cfg.Workspace.NewPaneURL = value311 cfg.Workspace.NewPaneURL = value
@@ -305,6 +316,19 @@ func TestValidateConfig_WorkspaceNewPaneURLAllowsLocalPathLikeValues(t *testing.
305 }316 }
306 }317 }
307 318
319+func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *testing.T) {
320+ for _, value := range []string{"/missing/dumber/page.html", "./missing.html", "../missing.html", "~/missing-dumber-page.html"} {
321+ t.Run(value, func(t *testing.T) {
322+ cfg := DefaultConfig()
323+ cfg.Workspace.NewPaneURL = value
324+
325+ err := validateConfig(cfg)
326+ require.Error(t, err)
327+ assert.Contains(t, err.Error(), "workspace.new_pane_url local path must exist")
328+ })
329+ }
330+}
331+
308 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingBareRelativeFile(t *testing.T) {332 func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingBareRelativeFile(t *testing.T) {
309 tmpDir := t.TempDir()333 tmpDir := t.TempDir()
310 oldWD, err := os.Getwd()334 oldWD, err := os.Getwd()
M internal/ui/app_browser_launch_test.go +12 -12
@@ -1104,7 +1104,7 @@ func TestApp_OpenFreshWindowRecordsTabOwnership(t *testing.T) {
11041104 created := &browserWindow{id: "window-1", tabs: entity.NewTabList()}
11051105 app := &App{
11061106 tabs: entity.NewTabList(),
1107- tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }),
1107+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }, nil),
11081108 browserWindows: map[string]*browserWindow{existingWindow.id: existingWindow},
11091109 windowForTab: map[entity.TabID]*browserWindow{existingTab.ID: existingWindow},
11101110 workspaceViews: map[entity.TabID]*component.WorkspaceView{entity.TabID("id-1"): &component.WorkspaceView{}},
@@ -1167,7 +1167,7 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
11671167 ctx := context.Background()
11681168 sessionID := entity.SessionID("session-empty-windows-fallback")
11691169 state := entity.SnapshotFromWindowTabLists(sessionID, []entity.WindowTabListState{}, 0, time.Unix(123, 0))
1170- tabsUC := usecase.NewManageTabsUseCase(func() string { return "fallback-tab" })
1170+ tabsUC := usecase.NewManageTabsUseCase(func() string { return "fallback-tab" }, nil)
11711171 windowTabs := entity.NewTabList()
11721172 bw := &browserWindow{id: "window-1", tabs: windowTabs}
11731173 app := &App{
@@ -1361,7 +1361,7 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
13611361
13621362 app := &App{
13631363 tabs: entity.NewTabList(),
1364- tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }),
1364+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }, nil),
13651365 browserWindows: map[string]*browserWindow{existingWindow.id: existingWindow},
13661366 windowForTab: map[entity.TabID]*browserWindow{existingTab.ID: existingWindow},
13671367 browserWindowFactory: func(context.Context, string) (*browserWindow, error) {
@@ -1403,7 +1403,7 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
14031403 setWindowTabBar(t, oldWindow, newTestTabBarShell(t))
14041404 setWindowTabBar(t, newWindow, newTestTabBarShell(t, createdTabID))
14051405 tabs := entity.NewTabList()
1406- tabsUC := usecase.NewManageTabsUseCase(func() string { return string(createdTabID) })
1406+ tabsUC := usecase.NewManageTabsUseCase(func() string { return string(createdTabID) }, nil)
14071407
14081408 existingTab := entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))
14091409 existingTabs := entity.NewTabList()
@@ -2280,7 +2280,7 @@ func TestApp_CreatePopupTabUsesParentPaneOwnerWhenFocusIsStale(t *testing.T) {
22802280 deps: &Dependencies{},
22812281 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
22822282 tabs: globalTabs,
2283- tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),
2283+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }, nil),
22842284 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
22852285 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
22862286 workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
@@ -2341,7 +2341,7 @@ func TestApp_TabCoordinatorCallbacksUseTargetWindowWhenFocusIsStale(t *testing.T
23412341 deps: &Dependencies{},
23422342 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
23432343 tabs: entity.NewTabList(),
2344- tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),
2344+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }, nil),
23452345 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
23462346 lastFocusedWindowID: first.id,
23472347 mainWindow: first.mainWindow,
@@ -2973,7 +2973,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexUsesSourceWindow(t *testin
29732973 app := &App{
29742974 deps: &Dependencies{},
29752975 runtimeConfig: runtimeConfigStateForTest(cfg),
2976- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
2976+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
29772977 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
29782978 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},
29792979 lastFocusedWindowID: first.id, // stale — should NOT be used
@@ -3016,7 +3016,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexCreatesOnlyOneMissingTab(t
30163016 app := &App{
30173017 deps: &Dependencies{},
30183018 runtimeConfig: runtimeConfigStateForTest(cfg),
3019- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3019+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
30203020 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
30213021 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
30223022 lastFocusedWindowID: first.id, // stale — should NOT be used
@@ -3062,7 +3062,7 @@ func TestApp_SwitchBrowserWindowTabIndexNegativeIndexIsIgnored(t *testing.T) {
30623062 app := &App{
30633063 deps: &Dependencies{},
30643064 runtimeConfig: runtimeConfigStateForTest(cfg),
3065- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3065+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
30663066 browserWindows: map[string]*browserWindow{second.id: second},
30673067 windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
30683068 }
@@ -3089,7 +3089,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexNegativeDoesNotMutateWindo
30893089 app := &App{
30903090 deps: &Dependencies{},
30913091 runtimeConfig: runtimeConfigStateForTest(cfg),
3092- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3092+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
30933093 browserWindows: map[string]*browserWindow{second.id: second},
30943094 lastFocusedWindowID: "window-1",
30953095 }
@@ -3119,7 +3119,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexMissingURLDoesNotCreateTab
31193119 app := &App{
31203120 deps: &Dependencies{},
31213121 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3122- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3122+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
31233123 browserWindows: map[string]*browserWindow{second.id: second},
31243124 windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
31253125 }
@@ -3145,7 +3145,7 @@ func TestApp_SwitchBrowserWindowTabIndexMissingURLDoesNotMutateWindowState(t *te
31453145 app := &App{
31463146 deps: &Dependencies{},
31473147 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3148- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3148+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
31493149 browserWindows: map[string]*browserWindow{second.id: second},
31503150 lastFocusedWindowID: "window-1",
31513151 }
@@ -1104,7 +1104,7 @@ func TestApp_OpenFreshWindowRecordsTabOwnership(t *testing.T) {
1104 created := &browserWindow{id: "window-1", tabs: entity.NewTabList()}1104 created := &browserWindow{id: "window-1", tabs: entity.NewTabList()}
1105 app := &App{1105 app := &App{
1106 tabs: entity.NewTabList(),1106 tabs: entity.NewTabList(),
1107- tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }),1107+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }, nil),
1108 browserWindows: map[string]*browserWindow{existingWindow.id: existingWindow},1108 browserWindows: map[string]*browserWindow{existingWindow.id: existingWindow},
1109 windowForTab: map[entity.TabID]*browserWindow{existingTab.ID: existingWindow},1109 windowForTab: map[entity.TabID]*browserWindow{existingTab.ID: existingWindow},
1110 workspaceViews: map[entity.TabID]*component.WorkspaceView{entity.TabID("id-1"): &component.WorkspaceView{}},1110 workspaceViews: map[entity.TabID]*component.WorkspaceView{entity.TabID("id-1"): &component.WorkspaceView{}},
@@ -1167,7 +1167,7 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
1167 ctx := context.Background()1167 ctx := context.Background()
1168 sessionID := entity.SessionID("session-empty-windows-fallback")1168 sessionID := entity.SessionID("session-empty-windows-fallback")
1169 state := entity.SnapshotFromWindowTabLists(sessionID, []entity.WindowTabListState{}, 0, time.Unix(123, 0))1169 state := entity.SnapshotFromWindowTabLists(sessionID, []entity.WindowTabListState{}, 0, time.Unix(123, 0))
1170- tabsUC := usecase.NewManageTabsUseCase(func() string { return "fallback-tab" })1170+ tabsUC := usecase.NewManageTabsUseCase(func() string { return "fallback-tab" }, nil)
1171 windowTabs := entity.NewTabList()1171 windowTabs := entity.NewTabList()
1172 bw := &browserWindow{id: "window-1", tabs: windowTabs}1172 bw := &browserWindow{id: "window-1", tabs: windowTabs}
1173 app := &App{1173 app := &App{
@@ -1361,7 +1361,7 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
1361 1361
1362 app := &App{1362 app := &App{
1363 tabs: entity.NewTabList(),1363 tabs: entity.NewTabList(),
1364- tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }),1364+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }, nil),
1365 browserWindows: map[string]*browserWindow{existingWindow.id: existingWindow},1365 browserWindows: map[string]*browserWindow{existingWindow.id: existingWindow},
1366 windowForTab: map[entity.TabID]*browserWindow{existingTab.ID: existingWindow},1366 windowForTab: map[entity.TabID]*browserWindow{existingTab.ID: existingWindow},
1367 browserWindowFactory: func(context.Context, string) (*browserWindow, error) {1367 browserWindowFactory: func(context.Context, string) (*browserWindow, error) {
@@ -1403,7 +1403,7 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
1403 setWindowTabBar(t, oldWindow, newTestTabBarShell(t))1403 setWindowTabBar(t, oldWindow, newTestTabBarShell(t))
1404 setWindowTabBar(t, newWindow, newTestTabBarShell(t, createdTabID))1404 setWindowTabBar(t, newWindow, newTestTabBarShell(t, createdTabID))
1405 tabs := entity.NewTabList()1405 tabs := entity.NewTabList()
1406- tabsUC := usecase.NewManageTabsUseCase(func() string { return string(createdTabID) })1406+ tabsUC := usecase.NewManageTabsUseCase(func() string { return string(createdTabID) }, nil)
1407 1407
1408 existingTab := entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))1408 existingTab := entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))
1409 existingTabs := entity.NewTabList()1409 existingTabs := entity.NewTabList()
@@ -2280,7 +2280,7 @@ func TestApp_CreatePopupTabUsesParentPaneOwnerWhenFocusIsStale(t *testing.T) {
2280 deps: &Dependencies{},2280 deps: &Dependencies{},
2281 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),2281 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
2282 tabs: globalTabs,2282 tabs: globalTabs,
2283- tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),2283+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }, nil),
2284 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2284 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
2285 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},2285 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
2286 workspaceViews: make(map[entity.TabID]*component.WorkspaceView),2286 workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
@@ -2341,7 +2341,7 @@ func TestApp_TabCoordinatorCallbacksUseTargetWindowWhenFocusIsStale(t *testing.T
2341 deps: &Dependencies{},2341 deps: &Dependencies{},
2342 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),2342 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
2343 tabs: entity.NewTabList(),2343 tabs: entity.NewTabList(),
2344- tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),2344+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }, nil),
2345 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2345 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
2346 lastFocusedWindowID: first.id,2346 lastFocusedWindowID: first.id,
2347 mainWindow: first.mainWindow,2347 mainWindow: first.mainWindow,
@@ -2973,7 +2973,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexUsesSourceWindow(t *testin
2973 app := &App{2973 app := &App{
2974 deps: &Dependencies{},2974 deps: &Dependencies{},
2975 runtimeConfig: runtimeConfigStateForTest(cfg),2975 runtimeConfig: runtimeConfigStateForTest(cfg),
2976- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),2976+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
2977 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2977 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
2978 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},2978 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},
2979 lastFocusedWindowID: first.id, // stale — should NOT be used2979 lastFocusedWindowID: first.id, // stale — should NOT be used
@@ -3016,7 +3016,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexCreatesOnlyOneMissingTab(t
3016 app := &App{3016 app := &App{
3017 deps: &Dependencies{},3017 deps: &Dependencies{},
3018 runtimeConfig: runtimeConfigStateForTest(cfg),3018 runtimeConfig: runtimeConfigStateForTest(cfg),
3019- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3019+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
3020 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},3020 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
3021 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},3021 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
3022 lastFocusedWindowID: first.id, // stale — should NOT be used3022 lastFocusedWindowID: first.id, // stale — should NOT be used
@@ -3062,7 +3062,7 @@ func TestApp_SwitchBrowserWindowTabIndexNegativeIndexIsIgnored(t *testing.T) {
3062 app := &App{3062 app := &App{
3063 deps: &Dependencies{},3063 deps: &Dependencies{},
3064 runtimeConfig: runtimeConfigStateForTest(cfg),3064 runtimeConfig: runtimeConfigStateForTest(cfg),
3065- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3065+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
3066 browserWindows: map[string]*browserWindow{second.id: second},3066 browserWindows: map[string]*browserWindow{second.id: second},
3067 windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},3067 windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3068 }3068 }
@@ -3089,7 +3089,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexNegativeDoesNotMutateWindo
3089 app := &App{3089 app := &App{
3090 deps: &Dependencies{},3090 deps: &Dependencies{},
3091 runtimeConfig: runtimeConfigStateForTest(cfg),3091 runtimeConfig: runtimeConfigStateForTest(cfg),
3092- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3092+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
3093 browserWindows: map[string]*browserWindow{second.id: second},3093 browserWindows: map[string]*browserWindow{second.id: second},
3094 lastFocusedWindowID: "window-1",3094 lastFocusedWindowID: "window-1",
3095 }3095 }
@@ -3119,7 +3119,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexMissingURLDoesNotCreateTab
3119 app := &App{3119 app := &App{
3120 deps: &Dependencies{},3120 deps: &Dependencies{},
3121 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),3121 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3122- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3122+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
3123 browserWindows: map[string]*browserWindow{second.id: second},3123 browserWindows: map[string]*browserWindow{second.id: second},
3124 windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},3124 windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3125 }3125 }
@@ -3145,7 +3145,7 @@ func TestApp_SwitchBrowserWindowTabIndexMissingURLDoesNotMutateWindowState(t *te
3145 app := &App{3145 app := &App{
3146 deps: &Dependencies{},3146 deps: &Dependencies{},
3147 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),3147 runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3148- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3148+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
3149 browserWindows: map[string]*browserWindow{second.id: second},3149 browserWindows: map[string]*browserWindow{second.id: second},
3150 lastFocusedWindowID: "window-1",3150 lastFocusedWindowID: "window-1",
3151 }3151 }
M internal/ui/browser_window_history_sidebar_test.go +1 -1
@@ -709,7 +709,7 @@ func TestApp_HistorySidebarConfig_OpenInNewPaneCallback(t *testing.T) {
709709
710710 bw := &browserWindow{id: "window-1", tabs: bwTabs}
711711
712- panesUC := usecase.NewManagePanesUseCase(func() string { return "pane-2" })
712+ panesUC := usecase.NewManagePanesUseCase(func() string { return "pane-2" }, nil)
713713 wsCoord := coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
714714 PanesUC: panesUC,
715715 GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
@@ -709,7 +709,7 @@ func TestApp_HistorySidebarConfig_OpenInNewPaneCallback(t *testing.T) {
709 709
710 bw := &browserWindow{id: "window-1", tabs: bwTabs}710 bw := &browserWindow{id: "window-1", tabs: bwTabs}
711 711
712- panesUC := usecase.NewManagePanesUseCase(func() string { return "pane-2" })712+ panesUC := usecase.NewManagePanesUseCase(func() string { return "pane-2" }, nil)
713 wsCoord := coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{713 wsCoord := coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
714 PanesUC: panesUC,714 PanesUC: panesUC,
715 GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {715 GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
M internal/ui/browser_window_test.go +4 -4
@@ -139,7 +139,7 @@ func TestTabCoordinator_CloseReleasesClosedTabWorkspaceWebViews(t *testing.T) {
139139 app := &App{
140140 deps: &Dependencies{},
141141 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
142- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
142+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
143143 contentCoord: contentCoord,
144144 browserWindows: map[string]*browserWindow{bw.id: bw},
145145 tabs: entity.NewTabList(),
@@ -183,7 +183,7 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
183183 app := &App{
184184 deps: &Dependencies{},
185185 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
186- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
186+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
187187 contentCoord: contentCoord,
188188 browserWindows: map[string]*browserWindow{bw.id: bw},
189189 tabs: entity.NewTabList(),
@@ -218,7 +218,7 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
218218 app := &App{
219219 deps: &Dependencies{},
220220 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
221- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
221+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
222222 contentCoord: contentCoord,
223223 browserWindows: map[string]*browserWindow{bw.id: bw},
224224 tabs: entity.NewTabList(),
@@ -462,7 +462,7 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) {
462462 func TestOpenFreshWindow_FirstWindowRegistersShellWithoutCreatingTab(t *testing.T) {
463463 app := &App{
464464 tabs: entity.NewTabList(),
465- tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }),
465+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }, nil),
466466 browserWindows: make(map[string]*browserWindow),
467467 }
468468 app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {
@@ -139,7 +139,7 @@ func TestTabCoordinator_CloseReleasesClosedTabWorkspaceWebViews(t *testing.T) {
139 app := &App{139 app := &App{
140 deps: &Dependencies{},140 deps: &Dependencies{},
141 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),141 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
142- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),142+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
143 contentCoord: contentCoord,143 contentCoord: contentCoord,
144 browserWindows: map[string]*browserWindow{bw.id: bw},144 browserWindows: map[string]*browserWindow{bw.id: bw},
145 tabs: entity.NewTabList(),145 tabs: entity.NewTabList(),
@@ -183,7 +183,7 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
183 app := &App{183 app := &App{
184 deps: &Dependencies{},184 deps: &Dependencies{},
185 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),185 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
186- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),186+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
187 contentCoord: contentCoord,187 contentCoord: contentCoord,
188 browserWindows: map[string]*browserWindow{bw.id: bw},188 browserWindows: map[string]*browserWindow{bw.id: bw},
189 tabs: entity.NewTabList(),189 tabs: entity.NewTabList(),
@@ -218,7 +218,7 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
218 app := &App{218 app := &App{
219 deps: &Dependencies{},219 deps: &Dependencies{},
220 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),220 runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
221- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),221+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil),
222 contentCoord: contentCoord,222 contentCoord: contentCoord,
223 browserWindows: map[string]*browserWindow{bw.id: bw},223 browserWindows: map[string]*browserWindow{bw.id: bw},
224 tabs: entity.NewTabList(),224 tabs: entity.NewTabList(),
@@ -462,7 +462,7 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) {
462 func TestOpenFreshWindow_FirstWindowRegistersShellWithoutCreatingTab(t *testing.T) {462 func TestOpenFreshWindow_FirstWindowRegistersShellWithoutCreatingTab(t *testing.T) {
463 app := &App{463 app := &App{
464 tabs: entity.NewTabList(),464 tabs: entity.NewTabList(),
465- tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }),465+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }, nil),
466 browserWindows: make(map[string]*browserWindow),466 browserWindows: make(map[string]*browserWindow),
467 }467 }
468 app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {468 app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {
M internal/ui/coordinator/tab_test.go +5 -5
@@ -26,7 +26,7 @@ func TestTabCoordinator_CloseTargetEmptyDoesNotQuitGlobally(t *testing.T) {
2626 secondTabs.Add(entity.NewTab(entity.TabID("second-tab"), entity.WorkspaceID("ws-second"), entity.NewPane(entity.PaneID("pane-second"))))
2727
2828 gen := counterIDGen()
29- tabsUC := usecase.NewManageTabsUseCase(gen)
29+ tabsUC := usecase.NewManageTabsUseCase(gen, nil)
3030
3131 coord := NewTabCoordinator(ctx, TabCoordinatorConfig{
3232 TabsUC: tabsUC,
@@ -124,7 +124,7 @@ func TestTabCoordinator_SwitchByIndexUsesTargetTabListOnly(t *testing.T) {
124124 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-2"), entity.WorkspaceID("ws-second-2"), entity.NewPane(entity.PaneID("pane-second-2"))))
125125
126126 gen := counterIDGen()
127- tabsUC := usecase.NewManageTabsUseCase(gen)
127+ tabsUC := usecase.NewManageTabsUseCase(gen, nil)
128128
129129 // Create TabCoordinator without global tabs (target-driven).
130130 coord := NewTabCoordinator(ctx, TabCoordinatorConfig{
@@ -175,7 +175,7 @@ func TestTabCoordinator_CreateAndSwitchCallbacksCarryTarget(t *testing.T) {
175175 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-1"), entity.WorkspaceID("ws-second-1"), entity.NewPane(entity.PaneID("pane-second-1"))))
176176 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-2"), entity.WorkspaceID("ws-second-2"), entity.NewPane(entity.PaneID("pane-second-2"))))
177177
178- coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen())})
178+ coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil)})
179179 coord.SetCurrentTarget(TabTarget{Tabs: firstTabs})
180180
181181 secondTarget := TabTarget{Tabs: secondTabs}
@@ -222,7 +222,7 @@ func TestTabCoordinator_SwitchByIndexCreatingCreatesMissingTabs(t *testing.T) {
222222 secondTabs := entity.NewTabList()
223223 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-1"), entity.WorkspaceID("ws-second-1"), entity.NewPane(entity.PaneID("pane-second-1"))))
224224
225- coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen())})
225+ coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil)})
226226
227227 secondTarget := TabTarget{Tabs: secondTabs}
228228
@@ -260,7 +260,7 @@ func TestTabCoordinator_SwitchByIndexCreatingMissingURLError(t *testing.T) {
260260 targetTabs := entity.NewTabList()
261261 targetTabs.Add(entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1"))))
262262
263- coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen())})
263+ coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil)})
264264
265265 target := TabTarget{Tabs: targetTabs}
266266
@@ -26,7 +26,7 @@ func TestTabCoordinator_CloseTargetEmptyDoesNotQuitGlobally(t *testing.T) {
26 secondTabs.Add(entity.NewTab(entity.TabID("second-tab"), entity.WorkspaceID("ws-second"), entity.NewPane(entity.PaneID("pane-second"))))26 secondTabs.Add(entity.NewTab(entity.TabID("second-tab"), entity.WorkspaceID("ws-second"), entity.NewPane(entity.PaneID("pane-second"))))
27 27
28 gen := counterIDGen()28 gen := counterIDGen()
29- tabsUC := usecase.NewManageTabsUseCase(gen)29+ tabsUC := usecase.NewManageTabsUseCase(gen, nil)
30 30
31 coord := NewTabCoordinator(ctx, TabCoordinatorConfig{31 coord := NewTabCoordinator(ctx, TabCoordinatorConfig{
32 TabsUC: tabsUC,32 TabsUC: tabsUC,
@@ -124,7 +124,7 @@ func TestTabCoordinator_SwitchByIndexUsesTargetTabListOnly(t *testing.T) {
124 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-2"), entity.WorkspaceID("ws-second-2"), entity.NewPane(entity.PaneID("pane-second-2"))))124 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-2"), entity.WorkspaceID("ws-second-2"), entity.NewPane(entity.PaneID("pane-second-2"))))
125 125
126 gen := counterIDGen()126 gen := counterIDGen()
127- tabsUC := usecase.NewManageTabsUseCase(gen)127+ tabsUC := usecase.NewManageTabsUseCase(gen, nil)
128 128
129 // Create TabCoordinator without global tabs (target-driven).129 // Create TabCoordinator without global tabs (target-driven).
130 coord := NewTabCoordinator(ctx, TabCoordinatorConfig{130 coord := NewTabCoordinator(ctx, TabCoordinatorConfig{
@@ -175,7 +175,7 @@ func TestTabCoordinator_CreateAndSwitchCallbacksCarryTarget(t *testing.T) {
175 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-1"), entity.WorkspaceID("ws-second-1"), entity.NewPane(entity.PaneID("pane-second-1"))))175 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-1"), entity.WorkspaceID("ws-second-1"), entity.NewPane(entity.PaneID("pane-second-1"))))
176 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-2"), entity.WorkspaceID("ws-second-2"), entity.NewPane(entity.PaneID("pane-second-2"))))176 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-2"), entity.WorkspaceID("ws-second-2"), entity.NewPane(entity.PaneID("pane-second-2"))))
177 177
178- coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen())})178+ coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil)})
179 coord.SetCurrentTarget(TabTarget{Tabs: firstTabs})179 coord.SetCurrentTarget(TabTarget{Tabs: firstTabs})
180 180
181 secondTarget := TabTarget{Tabs: secondTabs}181 secondTarget := TabTarget{Tabs: secondTabs}
@@ -222,7 +222,7 @@ func TestTabCoordinator_SwitchByIndexCreatingCreatesMissingTabs(t *testing.T) {
222 secondTabs := entity.NewTabList()222 secondTabs := entity.NewTabList()
223 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-1"), entity.WorkspaceID("ws-second-1"), entity.NewPane(entity.PaneID("pane-second-1"))))223 secondTabs.Add(entity.NewTab(entity.TabID("second-tab-1"), entity.WorkspaceID("ws-second-1"), entity.NewPane(entity.PaneID("pane-second-1"))))
224 224
225- coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen())})225+ coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil)})
226 226
227 secondTarget := TabTarget{Tabs: secondTabs}227 secondTarget := TabTarget{Tabs: secondTabs}
228 228
@@ -260,7 +260,7 @@ func TestTabCoordinator_SwitchByIndexCreatingMissingURLError(t *testing.T) {
260 targetTabs := entity.NewTabList()260 targetTabs := entity.NewTabList()
261 targetTabs.Add(entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1"))))261 targetTabs.Add(entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1"))))
262 262
263- coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen())})263+ coord := NewTabCoordinator(ctx, TabCoordinatorConfig{TabsUC: usecase.NewManageTabsUseCase(counterIDGen(), nil)})
264 264
265 target := TabTarget{Tabs: targetTabs}265 target := TabTarget{Tabs: targetTabs}
266 266
M internal/ui/coordinator/workspace.go +15 -15
@@ -1460,24 +1460,23 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
14601460 return nil
14611461 }
14621462
1463- // Create new pane entity
1464- newPaneID := entity.PaneID(c.generateID())
1465- newPane := entity.NewPane(newPaneID)
1466- newPane.URI = c.panesUC.NormalizeNavigationURL(ctx, c.newPaneURL)
1467- newPane.Title = defaultPaneTitle
1468-
1469- // Determine if we need to create a new stack or add to existing
1463+ // Determine if we need to create a new stack or add to existing.
14701464 var stackNode *entity.PaneNode
1465+ var newPane *entity.Pane
1466+ var newPaneID entity.PaneID
14711467 var needsFirstPaneTitleUpdate bool
14721468
14731469 if stackCtx.activeNode.Parent != nil && stackCtx.activeNode.Parent.IsStacked {
14741470 // Already in a stack - use AddToStack use case
14751471 stackNode = stackCtx.activeNode.Parent
1476- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane)
1472+ output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, c.newPaneURL)
14771473 if err != nil {
14781474 log.Error().Err(err).Msg("failed to add pane to stack via use case")
14791475 return err
14801476 }
1477+ newPane = output.NewPaneNode.Pane
1478+ newPaneID = newPane.ID
1479+ newPane.Title = defaultPaneTitle
14811480 log.Debug().
14821481 Int("stack_size", len(stackNode.Children)).
14831482 Int("insert_index", output.StackIndex).
@@ -1485,28 +1484,29 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
14851484 } else if stackCtx.activeNode.IsStacked {
14861485 // Active node is already a stack container - add to it
14871486 stackNode = stackCtx.activeNode
1488- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane)
1487+ output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, c.newPaneURL)
14891488 if err != nil {
14901489 log.Error().Err(err).Msg("failed to add pane to stack via use case")
14911490 return err
14921491 }
1492+ newPane = output.NewPaneNode.Pane
1493+ newPaneID = newPane.ID
1494+ newPane.Title = defaultPaneTitle
14931495 log.Debug().
14941496 Int("stack_size", len(stackNode.Children)).
14951497 Int("insert_index", output.StackIndex).
14961498 Msg("added to stack container via use case")
14971499 } else {
1498- // Need to create a new stack - use CreateStack use case
1499- // But CreateStack creates its own pane, so we need a different approach
1500- // Convert to stack first, then the new pane is already created
1501- output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode)
1500+ // Need to create a new stack - use CreateStack use case.
1501+ output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode, c.newPaneURL)
15021502 if err != nil {
15031503 log.Error().Err(err).Msg("failed to create stack via use case")
15041504 return err
15051505 }
15061506 stackNode = output.StackNode
1507- // CreateStack creates its own new pane, use that instead
15081507 newPane = output.NewPane
15091508 newPaneID = newPane.ID
1509+ newPane.Title = defaultPaneTitle
15101510 needsFirstPaneTitleUpdate = true
15111511
15121512 // Update the original pane's title in the domain
@@ -2035,7 +2035,7 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input con
20352035 stackNode, conversionInfo := c.resolveOrCreateStackNode(ctx, parentNode, input.ParentPaneID)
20362036
20372037 // Add popup pane to stack using use case
2038- if _, err := c.panesUC.AddToStack(ctx, ws, stackNode, input.PopupPane); err != nil {
2038+ if _, err := c.panesUC.AddToStack(ctx, ws, stackNode, input.PopupPane, ""); err != nil {
20392039 // Rollback stack conversion if we created a new stack
20402040 conversionInfo.revert()
20412041 return fmt.Errorf("failed to add popup to stack: %w", err)
@@ -1460,24 +1460,23 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
1460 return nil1460 return nil
1461 }1461 }
1462 1462
1463- // Create new pane entity1463+ // Determine if we need to create a new stack or add to existing.
1464- newPaneID := entity.PaneID(c.generateID())
1465- newPane := entity.NewPane(newPaneID)
1466- newPane.URI = c.panesUC.NormalizeNavigationURL(ctx, c.newPaneURL)
1467- newPane.Title = defaultPaneTitle
1468-
1469- // Determine if we need to create a new stack or add to existing
1470 var stackNode *entity.PaneNode1464 var stackNode *entity.PaneNode
1465+ var newPane *entity.Pane
1466+ var newPaneID entity.PaneID
1471 var needsFirstPaneTitleUpdate bool1467 var needsFirstPaneTitleUpdate bool
1472 1468
1473 if stackCtx.activeNode.Parent != nil && stackCtx.activeNode.Parent.IsStacked {1469 if stackCtx.activeNode.Parent != nil && stackCtx.activeNode.Parent.IsStacked {
1474 // Already in a stack - use AddToStack use case1470 // Already in a stack - use AddToStack use case
1475 stackNode = stackCtx.activeNode.Parent1471 stackNode = stackCtx.activeNode.Parent
1476- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane)1472+ output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, c.newPaneURL)
1477 if err != nil {1473 if err != nil {
1478 log.Error().Err(err).Msg("failed to add pane to stack via use case")1474 log.Error().Err(err).Msg("failed to add pane to stack via use case")
1479 return err1475 return err
1480 }1476 }
1477+ newPane = output.NewPaneNode.Pane
1478+ newPaneID = newPane.ID
1479+ newPane.Title = defaultPaneTitle
1481 log.Debug().1480 log.Debug().
1482 Int("stack_size", len(stackNode.Children)).1481 Int("stack_size", len(stackNode.Children)).
1483 Int("insert_index", output.StackIndex).1482 Int("insert_index", output.StackIndex).
@@ -1485,28 +1484,29 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
1485 } else if stackCtx.activeNode.IsStacked {1484 } else if stackCtx.activeNode.IsStacked {
1486 // Active node is already a stack container - add to it1485 // Active node is already a stack container - add to it
1487 stackNode = stackCtx.activeNode1486 stackNode = stackCtx.activeNode
1488- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane)1487+ output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, c.newPaneURL)
1489 if err != nil {1488 if err != nil {
1490 log.Error().Err(err).Msg("failed to add pane to stack via use case")1489 log.Error().Err(err).Msg("failed to add pane to stack via use case")
1491 return err1490 return err
1492 }1491 }
1492+ newPane = output.NewPaneNode.Pane
1493+ newPaneID = newPane.ID
1494+ newPane.Title = defaultPaneTitle
1493 log.Debug().1495 log.Debug().
1494 Int("stack_size", len(stackNode.Children)).1496 Int("stack_size", len(stackNode.Children)).
1495 Int("insert_index", output.StackIndex).1497 Int("insert_index", output.StackIndex).
1496 Msg("added to stack container via use case")1498 Msg("added to stack container via use case")
1497 } else {1499 } else {
1498- // Need to create a new stack - use CreateStack use case1500+ // Need to create a new stack - use CreateStack use case.
1499- // But CreateStack creates its own pane, so we need a different approach1501+ output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode, c.newPaneURL)
1500- // Convert to stack first, then the new pane is already created
1501- output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode)
1502 if err != nil {1502 if err != nil {
1503 log.Error().Err(err).Msg("failed to create stack via use case")1503 log.Error().Err(err).Msg("failed to create stack via use case")
1504 return err1504 return err
1505 }1505 }
1506 stackNode = output.StackNode1506 stackNode = output.StackNode
1507- // CreateStack creates its own new pane, use that instead
1508 newPane = output.NewPane1507 newPane = output.NewPane
1509 newPaneID = newPane.ID1508 newPaneID = newPane.ID
1509+ newPane.Title = defaultPaneTitle
1510 needsFirstPaneTitleUpdate = true1510 needsFirstPaneTitleUpdate = true
1511 1511
1512 // Update the original pane's title in the domain1512 // Update the original pane's title in the domain
@@ -2035,7 +2035,7 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input con
2035 stackNode, conversionInfo := c.resolveOrCreateStackNode(ctx, parentNode, input.ParentPaneID)2035 stackNode, conversionInfo := c.resolveOrCreateStackNode(ctx, parentNode, input.ParentPaneID)
2036 2036
2037 // Add popup pane to stack using use case2037 // Add popup pane to stack using use case
2038- if _, err := c.panesUC.AddToStack(ctx, ws, stackNode, input.PopupPane); err != nil {2038+ if _, err := c.panesUC.AddToStack(ctx, ws, stackNode, input.PopupPane, ""); err != nil {
2039 // Rollback stack conversion if we created a new stack2039 // Rollback stack conversion if we created a new stack
2040 conversionInfo.revert()2040 conversionInfo.revert()
2041 return fmt.Errorf("failed to add popup to stack: %w", err)2041 return fmt.Errorf("failed to add popup to stack: %w", err)
M internal/ui/coordinator/workspace_systemview_test.go +3 -3
@@ -19,7 +19,7 @@ func TestWorkspaceCoordinator_ToggleSystemViewRightSplitsWithTargetURL(t *testin
1919 id := ids[idx]
2020 idx++
2121 return id
22- })
22+ }, nil)
2323
2424 initialPane := entity.NewPane("pane-1")
2525 initialPane.URI = "https://example.com"
@@ -56,7 +56,7 @@ func TestWorkspaceCoordinator_ToggleSystemViewRightFocusesExistingPane(t *testin
5656 PanesUC: usecase.NewManagePanesUseCase(func() string {
5757 t.Fatal("PanesUC should not be invoked when focusing an existing systemview pane")
5858 return "unused"
59- }),
59+ }, nil),
6060 GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
6161 return ws, nil
6262 },
@@ -71,7 +71,7 @@ func TestWorkspaceCoordinator_ToggleSystemViewRightFocusesExistingPane(t *testin
7171
7272 func TestWorkspaceCoordinator_ToggleSystemViewRightClosesActivePane(t *testing.T) {
7373 ctx := context.Background()
74- uc := usecase.NewManagePanesUseCase(func() string { return "unused" })
74+ uc := usecase.NewManagePanesUseCase(func() string { return "unused" }, nil)
7575 left := testLeafNode("pane-1")
7676 left.Pane.URI = "https://example.com"
7777 right := testLeafNode("pane-2")
@@ -19,7 +19,7 @@ func TestWorkspaceCoordinator_ToggleSystemViewRightSplitsWithTargetURL(t *testin
19 id := ids[idx]19 id := ids[idx]
20 idx++20 idx++
21 return id21 return id
22- })22+ }, nil)
23 23
24 initialPane := entity.NewPane("pane-1")24 initialPane := entity.NewPane("pane-1")
25 initialPane.URI = "https://example.com"25 initialPane.URI = "https://example.com"
@@ -56,7 +56,7 @@ func TestWorkspaceCoordinator_ToggleSystemViewRightFocusesExistingPane(t *testin
56 PanesUC: usecase.NewManagePanesUseCase(func() string {56 PanesUC: usecase.NewManagePanesUseCase(func() string {
57 t.Fatal("PanesUC should not be invoked when focusing an existing systemview pane")57 t.Fatal("PanesUC should not be invoked when focusing an existing systemview pane")
58 return "unused"58 return "unused"
59- }),59+ }, nil),
60 GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {60 GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
61 return ws, nil61 return ws, nil
62 },62 },
@@ -71,7 +71,7 @@ func TestWorkspaceCoordinator_ToggleSystemViewRightFocusesExistingPane(t *testin
71 71
72 func TestWorkspaceCoordinator_ToggleSystemViewRightClosesActivePane(t *testing.T) {72 func TestWorkspaceCoordinator_ToggleSystemViewRightClosesActivePane(t *testing.T) {
73 ctx := context.Background()73 ctx := context.Background()
74- uc := usecase.NewManagePanesUseCase(func() string { return "unused" })74+ uc := usecase.NewManagePanesUseCase(func() string { return "unused" }, nil)
75 left := testLeafNode("pane-1")75 left := testLeafNode("pane-1")
76 left.Pane.URI = "https://example.com"76 left.Pane.URI = "https://example.com"
77 right := testLeafNode("pane-2")77 right := testLeafNode("pane-2")

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close