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) { | |||
| 388 | 388 | ||
| 389 | 389 | // Session manager can work without a repo - it just shows an empty list | |
| 390 | 390 | var listSessionsUC *usecase.ListSessionsUseCase | |
| 391 | + | var deleteSessionUC *usecase.DeleteSessionUseCase | |
| 391 | 392 | if a.deps.SessionRepo != nil && a.deps.SessionStateRepo != nil { | |
| 392 | 393 | listSessionsUC = usecase.NewListSessionsUseCase( | |
| 393 | 394 | a.deps.SessionRepo, | |
| 394 | 395 | a.deps.SessionStateRepo, | |
| 395 | 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 | 405 | // Create session spawner for restoration | |
| @@ -401,8 +407,9 @@ func (a *App) initSessionManager(ctx context.Context) { | |||
| 401 | 407 | ||
| 402 | 408 | // Create session manager component | |
| 403 | 409 | 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, | |
| 406 | 413 | OnClose: func() { | |
| 407 | 414 | log.Debug().Msg("session manager closed") | |
| 408 | 415 | }, | |
| @@ -412,6 +419,11 @@ func (a *App) initSessionManager(ctx context.Context) { | |||
| 412 | 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 | 429 | 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 list | 389 | // Session manager can work without a repo - it just shows an empty list | ||
| 390 | var listSessionsUC *usecase.ListSessionsUseCase | 390 | 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 restoration | 405 | // Create session spawner for restoration | ||
| @@ -401,8 +407,9 @@ func (a *App) initSessionManager(ctx context.Context) { | |||||
| 401 | 407 | ||||
| 402 | // Create session manager component | 408 | // 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 { | |||
| 55 | 55 | populatingList bool // True while rebuilding list, to ignore selection callbacks | |
| 56 | 56 | ||
| 57 | 57 | // 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 | |
| 61 | 62 | ||
| 62 | 63 | // Callbacks | |
| 63 | 64 | onClose func() | |
| 64 | 65 | onOpen func(sessionID entity.SessionID) | |
| 66 | + | onToast func(ctx context.Context, message string, level ToastLevel) | |
| 65 | 67 | ||
| 66 | 68 | // GTK callback retention | |
| 67 | 69 | retainedCallbacks []interface{} | |
| @@ -71,11 +73,13 @@ type SessionManager struct { | |||
| 71 | 73 | ||
| 72 | 74 | // SessionManagerConfig holds configuration for creating a SessionManager. | |
| 73 | 75 | 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) | |
| 79 | 83 | } | |
| 80 | 84 | ||
| 81 | 85 | // NewSessionManager creates a new SessionManager component. | |
| @@ -83,14 +87,16 @@ func NewSessionManager(ctx context.Context, cfg SessionManagerConfig) *SessionMa | |||
| 83 | 87 | log := logging.FromContext(ctx) | |
| 84 | 88 | ||
| 85 | 89 | 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, | |
| 94 | 100 | } | |
| 95 | 101 | ||
| 96 | 102 | if err := sm.createWidgets(); err != nil { | |
| @@ -932,17 +938,43 @@ func (sm *SessionManager) deleteSelected() { | |||
| 932 | 938 | ||
| 933 | 939 | log := logging.FromContext(sm.ctx) | |
| 934 | 940 | ||
| 935 | - | // Don't delete current or active sessions | |
| 941 | + | // Don't delete current or active sessions (early check before async) | |
| 936 | 942 | if info.IsCurrent || info.IsActive { | |
| 937 | 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 | 947 | 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 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 | + | }() | |
| 946 | 978 | } | |
| 947 | 979 | ||
| 948 | 980 | // Destroy cleans up session manager resources. | |
| @@ -55,13 +55,15 @@ type SessionManager struct { | |||||
| 55 | populatingList bool // True while rebuilding list, to ignore selection callbacks | 55 | populatingList bool // True while rebuilding list, to ignore selection callbacks | ||
| 56 | 56 | ||||
| 57 | // Dependencies | 57 | // Dependencies | ||
| 58 | - | listSessionsUC *usecase.ListSessionsUseCase | 58 | + | listSessionsUC *usecase.ListSessionsUseCase |
| 59 | - | spawner port.SessionSpawner | 59 | + | deleteSessionUC *usecase.DeleteSessionUseCase |
| 60 | - | currentSession entity.SessionID | 60 | + | spawner port.SessionSpawner |
| 61 | + | currentSession entity.SessionID | |||
| 61 | 62 | ||||
| 62 | // Callbacks | 63 | // 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 retention | 68 | // 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.ListSessionsUseCase | 76 | + | ListSessionsUC *usecase.ListSessionsUseCase |
| 75 | - | Spawner port.SessionSpawner | 77 | + | DeleteSessionUC *usecase.DeleteSessionUseCase |
| 76 | - | CurrentSession entity.SessionID | 78 | + | 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 sessions | 941 | + | // 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 | return | 947 | 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 case | 955 | + | sessionID := info.Session.ID |
| 944 | - | // For now, just reload the list | 956 | + | 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. | ||