detached c654fc4

feat(ui): implement session deletion in session manager

commit
c654fc4396684347dcccb54d3b815107bdf11504
author
brice · · 2025-12-25 09:42 UTC
parent
f95e23c
signature
unsigned
2 files · +67 -23
M internal/ui/app.go +14 -2
@@ -388,12 +388,18 @@ func (a *App) initSessionManager(ctx context.Context) {
388388
389389 // Session manager can work without a repo - it just shows an empty list
390390 var listSessionsUC *usecase.ListSessionsUseCase
391+ var deleteSessionUC *usecase.DeleteSessionUseCase
391392 if a.deps.SessionRepo != nil && a.deps.SessionStateRepo != nil {
392393 listSessionsUC = usecase.NewListSessionsUseCase(
393394 a.deps.SessionRepo,
394395 a.deps.SessionStateRepo,
395396 a.deps.Config.Logging.LogDir,
396397 )
398+ deleteSessionUC = usecase.NewDeleteSessionUseCase(
399+ a.deps.SessionStateRepo,
400+ a.deps.SessionRepo,
401+ a.deps.Config.Logging.LogDir,
402+ )
397403 }
398404
399405 // Create session spawner for restoration
@@ -401,8 +407,9 @@ func (a *App) initSessionManager(ctx context.Context) {
401407
402408 // Create session manager component
403409 a.sessionManager = component.NewSessionManager(ctx, component.SessionManagerConfig{
404- ListSessionsUC: listSessionsUC,
405- CurrentSession: a.deps.CurrentSessionID,
410+ ListSessionsUC: listSessionsUC,
411+ DeleteSessionUC: deleteSessionUC,
412+ CurrentSession: a.deps.CurrentSessionID,
406413 OnClose: func() {
407414 log.Debug().Msg("session manager closed")
408415 },
@@ -412,6 +419,11 @@ func (a *App) initSessionManager(ctx context.Context) {
412419 log.Error().Err(err).Str("session_id", string(sessionID)).Msg("failed to spawn session")
413420 }
414421 },
422+ OnToast: func(ctx context.Context, message string, level component.ToastLevel) {
423+ if a.appToaster != nil {
424+ a.appToaster.Show(ctx, message, level)
425+ }
426+ },
415427 })
416428
417429 if a.sessionManager == nil {
@@ -388,12 +388,18 @@ func (a *App) initSessionManager(ctx context.Context) {
388 388
389 // Session manager can work without a repo - it just shows an empty list389 // Session manager can work without a repo - it just shows an empty list
390 var listSessionsUC *usecase.ListSessionsUseCase390 var listSessionsUC *usecase.ListSessionsUseCase
391+ var deleteSessionUC *usecase.DeleteSessionUseCase
391 if a.deps.SessionRepo != nil && a.deps.SessionStateRepo != nil {392 if a.deps.SessionRepo != nil && a.deps.SessionStateRepo != nil {
392 listSessionsUC = usecase.NewListSessionsUseCase(393 listSessionsUC = usecase.NewListSessionsUseCase(
393 a.deps.SessionRepo,394 a.deps.SessionRepo,
394 a.deps.SessionStateRepo,395 a.deps.SessionStateRepo,
395 a.deps.Config.Logging.LogDir,396 a.deps.Config.Logging.LogDir,
396 )397 )
398+ deleteSessionUC = usecase.NewDeleteSessionUseCase(
399+ a.deps.SessionStateRepo,
400+ a.deps.SessionRepo,
401+ a.deps.Config.Logging.LogDir,
402+ )
397 }403 }
398 404
399 // Create session spawner for restoration405 // Create session spawner for restoration
@@ -401,8 +407,9 @@ func (a *App) initSessionManager(ctx context.Context) {
401 407
402 // Create session manager component408 // Create session manager component
403 a.sessionManager = component.NewSessionManager(ctx, component.SessionManagerConfig{409 a.sessionManager = component.NewSessionManager(ctx, component.SessionManagerConfig{
404- ListSessionsUC: listSessionsUC,410+ ListSessionsUC: listSessionsUC,
405- CurrentSession: a.deps.CurrentSessionID,411+ DeleteSessionUC: deleteSessionUC,
412+ CurrentSession: a.deps.CurrentSessionID,
406 OnClose: func() {413 OnClose: func() {
407 log.Debug().Msg("session manager closed")414 log.Debug().Msg("session manager closed")
408 },415 },
@@ -412,6 +419,11 @@ func (a *App) initSessionManager(ctx context.Context) {
412 log.Error().Err(err).Str("session_id", string(sessionID)).Msg("failed to spawn session")419 log.Error().Err(err).Str("session_id", string(sessionID)).Msg("failed to spawn session")
413 }420 }
414 },421 },
422+ OnToast: func(ctx context.Context, message string, level component.ToastLevel) {
423+ if a.appToaster != nil {
424+ a.appToaster.Show(ctx, message, level)
425+ }
426+ },
415 })427 })
416 428
417 if a.sessionManager == nil {429 if a.sessionManager == nil {
M internal/ui/component/session_manager.go +53 -21
@@ -55,13 +55,15 @@ type SessionManager struct {
5555 populatingList bool // True while rebuilding list, to ignore selection callbacks
5656
5757 // Dependencies
58- listSessionsUC *usecase.ListSessionsUseCase
59- spawner port.SessionSpawner
60- currentSession entity.SessionID
58+ listSessionsUC *usecase.ListSessionsUseCase
59+ deleteSessionUC *usecase.DeleteSessionUseCase
60+ spawner port.SessionSpawner
61+ currentSession entity.SessionID
6162
6263 // Callbacks
6364 onClose func()
6465 onOpen func(sessionID entity.SessionID)
66+ onToast func(ctx context.Context, message string, level ToastLevel)
6567
6668 // GTK callback retention
6769 retainedCallbacks []interface{}
@@ -71,11 +73,13 @@ type SessionManager struct {
7173
7274 // SessionManagerConfig holds configuration for creating a SessionManager.
7375 type SessionManagerConfig struct {
74- ListSessionsUC *usecase.ListSessionsUseCase
75- Spawner port.SessionSpawner
76- CurrentSession entity.SessionID
77- OnClose func()
78- OnOpen func(sessionID entity.SessionID)
76+ ListSessionsUC *usecase.ListSessionsUseCase
77+ DeleteSessionUC *usecase.DeleteSessionUseCase
78+ Spawner port.SessionSpawner
79+ CurrentSession entity.SessionID
80+ OnClose func()
81+ OnOpen func(sessionID entity.SessionID)
82+ OnToast func(ctx context.Context, message string, level ToastLevel)
7983 }
8084
8185 // NewSessionManager creates a new SessionManager component.
@@ -83,14 +87,16 @@ func NewSessionManager(ctx context.Context, cfg SessionManagerConfig) *SessionMa
8387 log := logging.FromContext(ctx)
8488
8589 sm := &SessionManager{
86- ctx: ctx,
87- listSessionsUC: cfg.ListSessionsUC,
88- spawner: cfg.Spawner,
89- currentSession: cfg.CurrentSession,
90- onClose: cfg.OnClose,
91- onOpen: cfg.OnOpen,
92- selectedIndex: -1,
93- expandedIndex: -1,
90+ ctx: ctx,
91+ listSessionsUC: cfg.ListSessionsUC,
92+ deleteSessionUC: cfg.DeleteSessionUC,
93+ spawner: cfg.Spawner,
94+ currentSession: cfg.CurrentSession,
95+ onClose: cfg.OnClose,
96+ onOpen: cfg.OnOpen,
97+ onToast: cfg.OnToast,
98+ selectedIndex: -1,
99+ expandedIndex: -1,
94100 }
95101
96102 if err := sm.createWidgets(); err != nil {
@@ -932,17 +938,43 @@ func (sm *SessionManager) deleteSelected() {
932938
933939 log := logging.FromContext(sm.ctx)
934940
935- // Don't delete current or active sessions
941+ // Don't delete current or active sessions (early check before async)
936942 if info.IsCurrent || info.IsActive {
937943 log.Debug().Msg("cannot delete current or active session")
944+ if sm.onToast != nil {
945+ sm.onToast(sm.ctx, "Cannot delete active session", ToastWarning)
946+ }
938947 return
939948 }
940949
941- log.Info().Str("session_id", string(info.Session.ID)).Msg("deleting session")
950+ if sm.deleteSessionUC == nil {
951+ log.Error().Msg("delete session use case not available")
952+ return
953+ }
942954
943- // TODO: Implement session deletion via use case
944- // For now, just reload the list
945- go sm.loadSessions()
955+ sessionID := info.Session.ID
956+ log.Info().Str("session_id", string(sessionID)).Msg("deleting session")
957+
958+ go func() {
959+ err := sm.deleteSessionUC.Execute(sm.ctx, usecase.DeleteSessionInput{
960+ SessionID: sessionID,
961+ CurrentSessionID: sm.currentSession,
962+ })
963+
964+ cb := glib.SourceFunc(func(_ uintptr) bool {
965+ if err != nil {
966+ log.Error().Err(err).Msg("failed to delete session")
967+ if sm.onToast != nil {
968+ sm.onToast(sm.ctx, "Failed to delete session", ToastError)
969+ }
970+ } else if sm.onToast != nil {
971+ sm.onToast(sm.ctx, "Session deleted", ToastSuccess)
972+ }
973+ sm.loadSessions()
974+ return false
975+ })
976+ glib.IdleAdd(&cb, 0)
977+ }()
946978 }
947979
948980 // Destroy cleans up session manager resources.
@@ -55,13 +55,15 @@ type SessionManager struct {
55 populatingList bool // True while rebuilding list, to ignore selection callbacks55 populatingList bool // True while rebuilding list, to ignore selection callbacks
56 56
57 // Dependencies57 // Dependencies
58- listSessionsUC *usecase.ListSessionsUseCase58+ listSessionsUC *usecase.ListSessionsUseCase
59- spawner port.SessionSpawner59+ deleteSessionUC *usecase.DeleteSessionUseCase
60- currentSession entity.SessionID60+ spawner port.SessionSpawner
61+ currentSession entity.SessionID
61 62
62 // Callbacks63 // Callbacks
63 onClose func()64 onClose func()
64 onOpen func(sessionID entity.SessionID)65 onOpen func(sessionID entity.SessionID)
66+ onToast func(ctx context.Context, message string, level ToastLevel)
65 67
66 // GTK callback retention68 // GTK callback retention
67 retainedCallbacks []interface{}69 retainedCallbacks []interface{}
@@ -71,11 +73,13 @@ type SessionManager struct {
71 73
72 // SessionManagerConfig holds configuration for creating a SessionManager.74 // SessionManagerConfig holds configuration for creating a SessionManager.
73 type SessionManagerConfig struct {75 type SessionManagerConfig struct {
74- ListSessionsUC *usecase.ListSessionsUseCase76+ ListSessionsUC *usecase.ListSessionsUseCase
75- Spawner port.SessionSpawner77+ DeleteSessionUC *usecase.DeleteSessionUseCase
76- CurrentSession entity.SessionID78+ Spawner port.SessionSpawner
77- OnClose func()79+ CurrentSession entity.SessionID
78- OnOpen func(sessionID entity.SessionID)80+ OnClose func()
81+ OnOpen func(sessionID entity.SessionID)
82+ OnToast func(ctx context.Context, message string, level ToastLevel)
79 }83 }
80 84
81 // NewSessionManager creates a new SessionManager component.85 // NewSessionManager creates a new SessionManager component.
@@ -83,14 +87,16 @@ func NewSessionManager(ctx context.Context, cfg SessionManagerConfig) *SessionMa
83 log := logging.FromContext(ctx)87 log := logging.FromContext(ctx)
84 88
85 sm := &SessionManager{89 sm := &SessionManager{
86- ctx: ctx,90+ ctx: ctx,
87- listSessionsUC: cfg.ListSessionsUC,91+ listSessionsUC: cfg.ListSessionsUC,
88- spawner: cfg.Spawner,92+ deleteSessionUC: cfg.DeleteSessionUC,
89- currentSession: cfg.CurrentSession,93+ spawner: cfg.Spawner,
90- onClose: cfg.OnClose,94+ currentSession: cfg.CurrentSession,
91- onOpen: cfg.OnOpen,95+ onClose: cfg.OnClose,
92- selectedIndex: -1,96+ onOpen: cfg.OnOpen,
93- expandedIndex: -1,97+ onToast: cfg.OnToast,
98+ selectedIndex: -1,
99+ expandedIndex: -1,
94 }100 }
95 101
96 if err := sm.createWidgets(); err != nil {102 if err := sm.createWidgets(); err != nil {
@@ -932,17 +938,43 @@ func (sm *SessionManager) deleteSelected() {
932 938
933 log := logging.FromContext(sm.ctx)939 log := logging.FromContext(sm.ctx)
934 940
935- // Don't delete current or active sessions941+ // Don't delete current or active sessions (early check before async)
936 if info.IsCurrent || info.IsActive {942 if info.IsCurrent || info.IsActive {
937 log.Debug().Msg("cannot delete current or active session")943 log.Debug().Msg("cannot delete current or active session")
944+ if sm.onToast != nil {
945+ sm.onToast(sm.ctx, "Cannot delete active session", ToastWarning)
946+ }
938 return947 return
939 }948 }
940 949
941- log.Info().Str("session_id", string(info.Session.ID)).Msg("deleting session")950+ if sm.deleteSessionUC == nil {
951+ log.Error().Msg("delete session use case not available")
952+ return
953+ }
942 954
943- // TODO: Implement session deletion via use case955+ sessionID := info.Session.ID
944- // For now, just reload the list956+ log.Info().Str("session_id", string(sessionID)).Msg("deleting session")
945- go sm.loadSessions()957+
958+ go func() {
959+ err := sm.deleteSessionUC.Execute(sm.ctx, usecase.DeleteSessionInput{
960+ SessionID: sessionID,
961+ CurrentSessionID: sm.currentSession,
962+ })
963+
964+ cb := glib.SourceFunc(func(_ uintptr) bool {
965+ if err != nil {
966+ log.Error().Err(err).Msg("failed to delete session")
967+ if sm.onToast != nil {
968+ sm.onToast(sm.ctx, "Failed to delete session", ToastError)
969+ }
970+ } else if sm.onToast != nil {
971+ sm.onToast(sm.ctx, "Session deleted", ToastSuccess)
972+ }
973+ sm.loadSessions()
974+ return false
975+ })
976+ glib.IdleAdd(&cb, 0)
977+ }()
946 }978 }
947 979
948 // Destroy cleans up session manager resources.980 // Destroy cleans up session manager resources.

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