detached f95e23c

feat(cli): add session deletion command and TUI support

commit
f95e23c233930dc583a9548a1d21fcec86ddc5f4
author
brice · · 2025-12-25 09:42 UTC
parent
9f01528
signature
unsigned
3 files · +40 -41
M internal/cli/app.go +14 -15
@@ -33,9 +33,7 @@ type App struct {
3333 SessionUC *usecase.ManageSessionUseCase
3434 ListSessionsUC *usecase.ListSessionsUseCase
3535 RestoreUC *usecase.RestoreSessionUseCase
36-
37- // Repositories
38- SessionStateRepo repository.SessionStateRepository
36+ DeleteSessionUC *usecase.DeleteSessionUseCase
3937
4038 // Services
4139 FaviconService *favicon.Service
@@ -116,24 +114,25 @@ func NewApp() (*App, error) {
116114 searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo)
117115 listSessionsUC := usecase.NewListSessionsUseCase(sessionRepo, sessionStateRepo, cfg.Logging.LogDir)
118116 restoreUC := usecase.NewRestoreSessionUseCase(sessionStateRepo, sessionRepo)
117+ deleteSessionUC := usecase.NewDeleteSessionUseCase(sessionStateRepo, sessionRepo, cfg.Logging.LogDir)
119118
120119 // Create favicon service for CLI (path resolution for dmenu/fuzzel)
121120 faviconCacheDir, _ := config.GetFaviconCacheDir()
122121 faviconService := favicon.NewService(faviconCacheDir)
123122
124123 return &App{
125- Config: cfg,
126- Theme: theme,
127- db: db,
128- History: historyRepo,
129- SearchHistoryUC: searchHistoryUC,
130- SessionUC: sessionUC,
131- ListSessionsUC: listSessionsUC,
132- RestoreUC: restoreUC,
133- SessionStateRepo: sessionStateRepo,
134- FaviconService: faviconService,
135- ctx: ctx,
136- logCleanup: logCleanup,
124+ Config: cfg,
125+ Theme: theme,
126+ db: db,
127+ History: historyRepo,
128+ SearchHistoryUC: searchHistoryUC,
129+ SessionUC: sessionUC,
130+ ListSessionsUC: listSessionsUC,
131+ RestoreUC: restoreUC,
132+ DeleteSessionUC: deleteSessionUC,
133+ FaviconService: faviconService,
134+ ctx: ctx,
135+ logCleanup: logCleanup,
137136 }, nil
138137 }
139138
@@ -33,9 +33,7 @@ type App struct {
33 SessionUC *usecase.ManageSessionUseCase33 SessionUC *usecase.ManageSessionUseCase
34 ListSessionsUC *usecase.ListSessionsUseCase34 ListSessionsUC *usecase.ListSessionsUseCase
35 RestoreUC *usecase.RestoreSessionUseCase35 RestoreUC *usecase.RestoreSessionUseCase
36-36+ DeleteSessionUC *usecase.DeleteSessionUseCase
37- // Repositories
38- SessionStateRepo repository.SessionStateRepository
39 37
40 // Services38 // Services
41 FaviconService *favicon.Service39 FaviconService *favicon.Service
@@ -116,24 +114,25 @@ func NewApp() (*App, error) {
116 searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo)114 searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo)
117 listSessionsUC := usecase.NewListSessionsUseCase(sessionRepo, sessionStateRepo, cfg.Logging.LogDir)115 listSessionsUC := usecase.NewListSessionsUseCase(sessionRepo, sessionStateRepo, cfg.Logging.LogDir)
118 restoreUC := usecase.NewRestoreSessionUseCase(sessionStateRepo, sessionRepo)116 restoreUC := usecase.NewRestoreSessionUseCase(sessionStateRepo, sessionRepo)
117+ deleteSessionUC := usecase.NewDeleteSessionUseCase(sessionStateRepo, sessionRepo, cfg.Logging.LogDir)
119 118
120 // Create favicon service for CLI (path resolution for dmenu/fuzzel)119 // Create favicon service for CLI (path resolution for dmenu/fuzzel)
121 faviconCacheDir, _ := config.GetFaviconCacheDir()120 faviconCacheDir, _ := config.GetFaviconCacheDir()
122 faviconService := favicon.NewService(faviconCacheDir)121 faviconService := favicon.NewService(faviconCacheDir)
123 122
124 return &App{123 return &App{
125- Config: cfg,124+ Config: cfg,
126- Theme: theme,125+ Theme: theme,
127- db: db,126+ db: db,
128- History: historyRepo,127+ History: historyRepo,
129- SearchHistoryUC: searchHistoryUC,128+ SearchHistoryUC: searchHistoryUC,
130- SessionUC: sessionUC,129+ SessionUC: sessionUC,
131- ListSessionsUC: listSessionsUC,130+ ListSessionsUC: listSessionsUC,
132- RestoreUC: restoreUC,131+ RestoreUC: restoreUC,
133- SessionStateRepo: sessionStateRepo,132+ DeleteSessionUC: deleteSessionUC,
134- FaviconService: faviconService,133+ FaviconService: faviconService,
135- ctx: ctx,134+ ctx: ctx,
136- logCleanup: logCleanup,135+ logCleanup: logCleanup,
137 }, nil136 }, nil
138 }137 }
139 138
M internal/cli/cmd/sessions.go +11 -7
@@ -59,7 +59,7 @@ func runSessions(_ *cobra.Command, _ []string) error {
5959 m := model.NewSessionsModel(app.Ctx(), app.Theme, model.SessionsModelConfig{
6060 ListSessionsUC: app.ListSessionsUC,
6161 RestoreUC: app.RestoreUC,
62- SessionStateRepo: app.SessionStateRepo,
62+ DeleteSessionUC: app.DeleteSessionUC,
6363 CurrentSession: currentSessionID,
6464 MaxListedSessions: app.Config.Session.MaxListedSessions,
6565 })
@@ -237,7 +237,7 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
237237 return fmt.Errorf("app not initialized")
238238 }
239239
240- if app.SessionStateRepo == nil || app.ListSessionsUC == nil {
240+ if app.DeleteSessionUC == nil || app.ListSessionsUC == nil {
241241 return fmt.Errorf("session management not available")
242242 }
243243
@@ -247,13 +247,17 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
247247 return err
248248 }
249249
250- // Check if session is active
251- if sessionInfo.IsActive || sessionInfo.IsCurrent {
252- return fmt.Errorf("cannot delete active session - close the browser first")
250+ // Get current session ID
251+ var currentSessionID entity.SessionID
252+ if active, activeErr := app.SessionUC.GetActiveSession(app.Ctx()); activeErr == nil && active != nil {
253+ currentSessionID = active.ID
253254 }
254255
255- // Delete the session state
256- if err := app.SessionStateRepo.DeleteSnapshot(app.Ctx(), sessionInfo.Session.ID); err != nil {
256+ // Delete using use case (handles validation internally)
257+ if err := app.DeleteSessionUC.Execute(app.Ctx(), usecase.DeleteSessionInput{
258+ SessionID: sessionInfo.Session.ID,
259+ CurrentSessionID: currentSessionID,
260+ }); err != nil {
257261 return fmt.Errorf("delete session: %w", err)
258262 }
259263
@@ -59,7 +59,7 @@ func runSessions(_ *cobra.Command, _ []string) error {
59 m := model.NewSessionsModel(app.Ctx(), app.Theme, model.SessionsModelConfig{59 m := model.NewSessionsModel(app.Ctx(), app.Theme, model.SessionsModelConfig{
60 ListSessionsUC: app.ListSessionsUC,60 ListSessionsUC: app.ListSessionsUC,
61 RestoreUC: app.RestoreUC,61 RestoreUC: app.RestoreUC,
62- SessionStateRepo: app.SessionStateRepo,62+ DeleteSessionUC: app.DeleteSessionUC,
63 CurrentSession: currentSessionID,63 CurrentSession: currentSessionID,
64 MaxListedSessions: app.Config.Session.MaxListedSessions,64 MaxListedSessions: app.Config.Session.MaxListedSessions,
65 })65 })
@@ -237,7 +237,7 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
237 return fmt.Errorf("app not initialized")237 return fmt.Errorf("app not initialized")
238 }238 }
239 239
240- if app.SessionStateRepo == nil || app.ListSessionsUC == nil {240+ if app.DeleteSessionUC == nil || app.ListSessionsUC == nil {
241 return fmt.Errorf("session management not available")241 return fmt.Errorf("session management not available")
242 }242 }
243 243
@@ -247,13 +247,17 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
247 return err247 return err
248 }248 }
249 249
250- // Check if session is active250+ // Get current session ID
251- if sessionInfo.IsActive || sessionInfo.IsCurrent {251+ var currentSessionID entity.SessionID
252- return fmt.Errorf("cannot delete active session - close the browser first")252+ if active, activeErr := app.SessionUC.GetActiveSession(app.Ctx()); activeErr == nil && active != nil {
253+ currentSessionID = active.ID
253 }254 }
254 255
255- // Delete the session state256+ // Delete using use case (handles validation internally)
256- if err := app.SessionStateRepo.DeleteSnapshot(app.Ctx(), sessionInfo.Session.ID); err != nil {257+ if err := app.DeleteSessionUC.Execute(app.Ctx(), usecase.DeleteSessionInput{
258+ SessionID: sessionInfo.Session.ID,
259+ CurrentSessionID: currentSessionID,
260+ }); err != nil {
257 return fmt.Errorf("delete session: %w", err)261 return fmt.Errorf("delete session: %w", err)
258 }262 }
259 263
M internal/cli/model/sessions.go +15 -19
@@ -39,13 +39,11 @@ type SessionsModel struct {
3939 maxListedSessions int
4040
4141 // Dependencies
42- ctx context.Context
43- listSessionsUC *usecase.ListSessionsUseCase
44- restoreUC *usecase.RestoreSessionUseCase
45- sessionStateRepo interface {
46- DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error
47- }
48- theme *styles.Theme
42+ ctx context.Context
43+ listSessionsUC *usecase.ListSessionsUseCase
44+ restoreUC *usecase.RestoreSessionUseCase
45+ deleteSessionUC *usecase.DeleteSessionUseCase
46+ theme *styles.Theme
4947 }
5048
5149 // sessionsKeyMap defines keybindings for the sessions browser.
@@ -113,11 +111,9 @@ func defaultSessionsKeyMap() sessionsKeyMap {
113111
114112 // SessionsModelConfig holds configuration for the sessions model.
115113 type SessionsModelConfig struct {
116- ListSessionsUC *usecase.ListSessionsUseCase
117- RestoreUC *usecase.RestoreSessionUseCase
118- SessionStateRepo interface {
119- DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error
120- }
114+ ListSessionsUC *usecase.ListSessionsUseCase
115+ RestoreUC *usecase.RestoreSessionUseCase
116+ DeleteSessionUC *usecase.DeleteSessionUseCase
121117 CurrentSession entity.SessionID
122118 MaxListedSessions int
123119 }
@@ -140,7 +136,7 @@ func NewSessionsModel(ctx context.Context, theme *styles.Theme, cfg SessionsMode
140136 ctx: ctx,
141137 listSessionsUC: cfg.ListSessionsUC,
142138 restoreUC: cfg.RestoreUC,
143- sessionStateRepo: cfg.SessionStateRepo,
139+ deleteSessionUC: cfg.DeleteSessionUC,
144140 currentSession: cfg.CurrentSession,
145141 theme: theme,
146142 }
@@ -312,14 +308,14 @@ func (m SessionsModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
312308
313309 func (m SessionsModel) deleteSession(sessionID entity.SessionID) tea.Cmd {
314310 return func() tea.Msg {
315- log := logging.FromContext(m.ctx)
316- log.Info().Str("session_id", string(sessionID)).Msg("deleting session")
317-
318- if m.sessionStateRepo == nil {
319- return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("session state repo not available")}
311+ if m.deleteSessionUC == nil {
312+ return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("delete session use case not available")}
320313 }
321314
322- err := m.sessionStateRepo.DeleteSnapshot(m.ctx, sessionID)
315+ err := m.deleteSessionUC.Execute(m.ctx, usecase.DeleteSessionInput{
316+ SessionID: sessionID,
317+ CurrentSessionID: m.currentSession,
318+ })
323319 return sessionDeletedMsg{sessionID: sessionID, err: err}
324320 }
325321 }
@@ -39,13 +39,11 @@ type SessionsModel struct {
39 maxListedSessions int39 maxListedSessions int
40 40
41 // Dependencies41 // Dependencies
42- ctx context.Context42+ ctx context.Context
43- listSessionsUC *usecase.ListSessionsUseCase43+ listSessionsUC *usecase.ListSessionsUseCase
44- restoreUC *usecase.RestoreSessionUseCase44+ restoreUC *usecase.RestoreSessionUseCase
45- sessionStateRepo interface {45+ deleteSessionUC *usecase.DeleteSessionUseCase
46- DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error46+ theme *styles.Theme
47- }
48- theme *styles.Theme
49 }47 }
50 48
51 // sessionsKeyMap defines keybindings for the sessions browser.49 // sessionsKeyMap defines keybindings for the sessions browser.
@@ -113,11 +111,9 @@ func defaultSessionsKeyMap() sessionsKeyMap {
113 111
114 // SessionsModelConfig holds configuration for the sessions model.112 // SessionsModelConfig holds configuration for the sessions model.
115 type SessionsModelConfig struct {113 type SessionsModelConfig struct {
116- ListSessionsUC *usecase.ListSessionsUseCase114+ ListSessionsUC *usecase.ListSessionsUseCase
117- RestoreUC *usecase.RestoreSessionUseCase115+ RestoreUC *usecase.RestoreSessionUseCase
118- SessionStateRepo interface {116+ DeleteSessionUC *usecase.DeleteSessionUseCase
119- DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error
120- }
121 CurrentSession entity.SessionID117 CurrentSession entity.SessionID
122 MaxListedSessions int118 MaxListedSessions int
123 }119 }
@@ -140,7 +136,7 @@ func NewSessionsModel(ctx context.Context, theme *styles.Theme, cfg SessionsMode
140 ctx: ctx,136 ctx: ctx,
141 listSessionsUC: cfg.ListSessionsUC,137 listSessionsUC: cfg.ListSessionsUC,
142 restoreUC: cfg.RestoreUC,138 restoreUC: cfg.RestoreUC,
143- sessionStateRepo: cfg.SessionStateRepo,139+ deleteSessionUC: cfg.DeleteSessionUC,
144 currentSession: cfg.CurrentSession,140 currentSession: cfg.CurrentSession,
145 theme: theme,141 theme: theme,
146 }142 }
@@ -312,14 +308,14 @@ func (m SessionsModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
312 308
313 func (m SessionsModel) deleteSession(sessionID entity.SessionID) tea.Cmd {309 func (m SessionsModel) deleteSession(sessionID entity.SessionID) tea.Cmd {
314 return func() tea.Msg {310 return func() tea.Msg {
315- log := logging.FromContext(m.ctx)311+ if m.deleteSessionUC == nil {
316- log.Info().Str("session_id", string(sessionID)).Msg("deleting session")312+ return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("delete session use case not available")}
317-
318- if m.sessionStateRepo == nil {
319- return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("session state repo not available")}
320 }313 }
321 314
322- err := m.sessionStateRepo.DeleteSnapshot(m.ctx, sessionID)315+ err := m.deleteSessionUC.Execute(m.ctx, usecase.DeleteSessionInput{
316+ SessionID: sessionID,
317+ CurrentSessionID: m.currentSession,
318+ })
323 return sessionDeletedMsg{sessionID: sessionID, err: err}319 return sessionDeletedMsg{sessionID: sessionID, err: err}
324 }320 }
325 }321 }

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