implement getsessionleaves json rpc endpoint to show only (#265)

* Add CLAUDE.md to mark TUI as archived (ENG-1507)

Add documentation file for AI assistants clarifying that the TUI
is archived and no new features should be added. Development
efforts should focus on the WUI instead.

* feat(hld): implement getSessionLeaves JSON-RPC endpoint

Add new JSON-RPC endpoint that returns only leaf nodes of session trees,
filtering out any sessions that have children. This reduces UI clutter
by showing only the most recent sessions in each branch.

- Add database migration 5 to create index on parent_session_id
- Implement HandleGetSessionLeaves that builds parent-child map and filters leaves
- Sort results by last activity (newest first)
- Return empty array instead of nil for consistency
- Add comprehensive unit tests covering various tree structures
- Add integration test for the new endpoint
- Create DatabasePath test utility for proper test isolation
- Fix integration tests to use isolated databases instead of user's database

* formatting

* refactor(hld): adopt DatabasePath test utility in SQLite store tests

Replace manual temp directory creation with the standardized DatabasePath
test utility helper. This improves test consistency and automatically handles
environment variable setup and cleanup.

Updated test functions:
- TestSQLiteStore: uses "sqlite" suffix
- TestGetSessionConversationWithParentChain: uses "sqlite-parent" suffix
- TestSQLiteStoreIntegration: uses "sqlite-integration" suffix
- TestSQLiteStorePersistence: uses "sqlite-persist" suffix
- TestSQLiteStoreWithMockSession: uses "sqlite-mock" suffix

* feat(hld): add getSessionLeaves method to all daemon clients

Implements Phase 2 of the SessionLeaves endpoint by adding client support
across all daemon client libraries (Go, TypeScript, and Rust). This method
returns only leaf sessions (sessions with no children) to reduce UI clutter
when users have multiple session continuations.

- Add GetSessionLeaves to Go client interface and implementation
- Add TypeScript types and method to DaemonClient
- Add Rust types, trait method, and Tauri command
- Register new command in Tauri invoke handler

* feat(wui): switch to getSessionLeaves endpoint for session display

Update WUI to use the new getSessionLeaves JSON-RPC endpoint instead of
listSessions. This ensures only leaf sessions (sessions without children)
are displayed, reducing clutter when users have multiple session
continuations.

Part of ENG-1491
This commit is contained in:
Allison Durham
2025-07-02 13:43:27 -07:00
committed by GitHub
parent 9d388f1fbe
commit c4acfdfc67
17 changed files with 520 additions and 21 deletions

View File

@@ -256,6 +256,15 @@ func (c *client) ListSessions() (*rpc.ListSessionsResponse, error) {
return &resp, nil
}
// GetSessionLeaves gets only the leaf sessions (sessions with no children)
func (c *client) GetSessionLeaves() (*rpc.GetSessionLeavesResponse, error) {
var resp rpc.GetSessionLeavesResponse
if err := c.call("getSessionLeaves", nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// ContinueSession continues an existing completed session with a new query
func (c *client) ContinueSession(req rpc.ContinueSessionRequest) (*rpc.ContinueSessionResponse, error) {
var resp rpc.ContinueSessionResponse

View File

@@ -18,6 +18,9 @@ type Client interface {
// ListSessions lists all active sessions
ListSessions() (*rpc.ListSessionsResponse, error)
// GetSessionLeaves gets only the leaf sessions (sessions with no children)
GetSessionLeaves() (*rpc.GetSessionLeavesResponse, error)
// InterruptSession interrupts a running session
InterruptSession(sessionID string) error

View File

@@ -22,6 +22,7 @@ import (
// TestSessionLaunchIntegration tests launching a session through the daemon
func TestSessionLaunchIntegration(t *testing.T) {
socketPath := testutil.SocketPath(t, "session")
_ = testutil.DatabasePath(t, "session") // This sets HUMANLAYER_DATABASE_PATH
// Set environment for test
os.Setenv("HUMANLAYER_DAEMON_SOCKET", socketPath)
@@ -227,6 +228,79 @@ func TestSessionLaunchIntegration(t *testing.T) {
}
})
// Test GetSessionLeaves - should return same sessions as listSessions for now since no hierarchy
t.Run("GetSessionLeaves", func(t *testing.T) {
// Connect to daemon
conn, err := net.Dial("unix", socketPath)
if err != nil {
t.Fatalf("Failed to connect to daemon: %v", err)
}
defer conn.Close()
// Send GetSessionLeaves request
reqData, _ := json.Marshal(map[string]interface{}{
"jsonrpc": "2.0",
"method": "getSessionLeaves",
"params": map[string]interface{}{},
"id": 3,
})
if _, err := conn.Write(append(reqData, '\n')); err != nil {
t.Fatalf("Failed to send request: %v", err)
}
// Read response
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB buffer for large responses
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
t.Fatalf("Scanner error: %v", err)
}
t.Fatal("Failed to read response")
}
var resp map[string]interface{}
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
// Check for error
if errObj, ok := resp["error"]; ok {
t.Fatalf("GetSessionLeaves failed: %v", errObj)
}
// Check result
result, ok := resp["result"].(map[string]interface{})
if !ok {
t.Fatalf("Invalid result type: %T", resp["result"])
}
sessions, ok := result["sessions"].([]interface{})
if !ok {
t.Fatalf("Invalid sessions type: %T", result["sessions"])
}
t.Logf("Found %d leaf sessions", len(sessions))
// Verify each session has expected fields
for i, sess := range sessions {
session := sess.(map[string]interface{})
if _, ok := session["id"]; !ok {
t.Errorf("Session %d missing id field", i)
}
if _, ok := session["run_id"]; !ok {
t.Errorf("Session %d missing run_id field", i)
}
if _, ok := session["status"]; !ok {
t.Errorf("Session %d missing status field", i)
}
// Check that parent_session_id field exists (even if empty)
if _, ok := session["parent_session_id"]; !ok {
t.Errorf("Session %d missing parent_session_id field", i)
}
}
})
// Shutdown daemon
cancel()
@@ -244,6 +318,7 @@ func TestSessionLaunchIntegration(t *testing.T) {
// TestConcurrentSessions tests launching multiple sessions concurrently
func TestConcurrentSessions(t *testing.T) {
socketPath := testutil.SocketPath(t, "concurrent")
_ = testutil.DatabasePath(t, "concurrent") // This sets HUMANLAYER_DATABASE_PATH
// Set environment for test
os.Setenv("HUMANLAYER_DAEMON_SOCKET", socketPath)

View File

@@ -30,3 +30,33 @@ func CreateTestSocket(t *testing.T) string {
t.Helper()
return SocketPath(t, "test")
}
// DatabasePath returns a temporary database path for testing.
// It automatically sets the HUMANLAYER_DATABASE_PATH environment variable
// and registers cleanup with t.Cleanup().
func DatabasePath(t *testing.T, suffix string) string {
t.Helper()
// Create a temporary directory for the test
tempDir := t.TempDir()
// Create database path
dbPath := fmt.Sprintf("%s/test-%s.db", tempDir, suffix)
// Set environment variable
oldPath := os.Getenv("HUMANLAYER_DATABASE_PATH")
if err := os.Setenv("HUMANLAYER_DATABASE_PATH", dbPath); err != nil {
t.Fatalf("Failed to set HUMANLAYER_DATABASE_PATH: %v", err)
}
// Register cleanup to restore original environment
t.Cleanup(func() {
if oldPath != "" {
_ = os.Setenv("HUMANLAYER_DATABASE_PATH", oldPath)
} else {
_ = os.Unsetenv("HUMANLAYER_DATABASE_PATH")
}
})
return dbPath
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
claudecode "github.com/humanlayer/humanlayer/claudecode-go"
@@ -127,6 +128,59 @@ func (h *SessionHandlers) HandleListSessions(ctx context.Context, params json.Ra
}, nil
}
// GetSessionLeavesRequest is the request for getting session leaves
type GetSessionLeavesRequest struct {
// Empty for now - could add filters later
}
// GetSessionLeavesResponse is the response for getting session leaves
type GetSessionLeavesResponse struct {
Sessions []session.Info `json:"sessions"`
}
// HandleGetSessionLeaves handles the GetSessionLeaves RPC method
func (h *SessionHandlers) HandleGetSessionLeaves(ctx context.Context, params json.RawMessage) (interface{}, error) {
// Parse request (even though it's empty for now)
var req GetSessionLeavesRequest
if params != nil {
if err := json.Unmarshal(params, &req); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
}
// Get all sessions from the manager
allSessions := h.manager.ListSessions()
// Build parent-to-children map
childrenMap := make(map[string][]string)
for _, session := range allSessions {
if session.ParentSessionID != "" {
childrenMap[session.ParentSessionID] = append(childrenMap[session.ParentSessionID], session.ID)
}
}
// Identify leaf sessions (sessions with no children)
leaves := make([]session.Info, 0) // Initialize to empty slice, not nil
for _, session := range allSessions {
children := childrenMap[session.ID]
// Include only if session has no children (is a leaf node)
if len(children) == 0 {
leaves = append(leaves, session)
}
}
// Sort by last activity (newest first)
sort.Slice(leaves, func(i, j int) bool {
return leaves[i].LastActivityAt.After(leaves[j].LastActivityAt)
})
return &GetSessionLeavesResponse{
Sessions: leaves,
}, nil
}
// HandleGetConversation handles the GetConversation RPC method
func (h *SessionHandlers) HandleGetConversation(ctx context.Context, params json.RawMessage) (interface{}, error) {
var req GetConversationRequest
@@ -325,6 +379,7 @@ func (h *SessionHandlers) HandleInterruptSession(ctx context.Context, params jso
func (h *SessionHandlers) Register(server *Server) {
server.Register("launchSession", h.HandleLaunchSession)
server.Register("listSessions", h.HandleListSessions)
server.Register("getSessionLeaves", h.HandleGetSessionLeaves)
server.Register("getConversation", h.HandleGetConversation)
server.Register("getSessionState", h.HandleGetSessionState)
server.Register("continueSession", h.HandleContinueSession)

View File

@@ -239,6 +239,250 @@ func TestHandleGetSessionState(t *testing.T) {
})
}
func TestHandleGetSessionLeaves(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockManager := session.NewMockSessionManager(ctrl)
mockStore := store.NewMockConversationStore(ctrl)
handlers := NewSessionHandlers(mockManager, mockStore)
t.Run("empty sessions list", func(t *testing.T) {
// Mock empty sessions
mockManager.EXPECT().
ListSessions().
Return([]session.Info{})
result, err := handlers.HandleGetSessionLeaves(context.Background(), nil)
require.NoError(t, err)
resp, ok := result.(*GetSessionLeavesResponse)
require.True(t, ok)
assert.Empty(t, resp.Sessions)
})
t.Run("single session with no parent or children", func(t *testing.T) {
// Single session is always a leaf
sessions := []session.Info{
{
ID: "sess-1",
RunID: "run-1",
ClaudeSessionID: "claude-1",
ParentSessionID: "",
Status: session.StatusCompleted,
StartTime: time.Now().Add(-time.Hour),
LastActivityAt: time.Now().Add(-30 * time.Minute),
Query: "Test query",
Summary: "Test summary",
},
}
mockManager.EXPECT().
ListSessions().
Return(sessions)
result, err := handlers.HandleGetSessionLeaves(context.Background(), nil)
require.NoError(t, err)
resp, ok := result.(*GetSessionLeavesResponse)
require.True(t, ok)
assert.Len(t, resp.Sessions, 1)
assert.Equal(t, "sess-1", resp.Sessions[0].ID)
})
t.Run("linear chain of sessions", func(t *testing.T) {
// A->B->C, should return only C
now := time.Now()
sessions := []session.Info{
{
ID: "sess-1",
ParentSessionID: "",
LastActivityAt: now.Add(-3 * time.Hour),
},
{
ID: "sess-2",
ParentSessionID: "sess-1",
LastActivityAt: now.Add(-2 * time.Hour),
},
{
ID: "sess-3",
ParentSessionID: "sess-2",
LastActivityAt: now.Add(-1 * time.Hour),
},
}
mockManager.EXPECT().
ListSessions().
Return(sessions)
result, err := handlers.HandleGetSessionLeaves(context.Background(), nil)
require.NoError(t, err)
resp, ok := result.(*GetSessionLeavesResponse)
require.True(t, ok)
assert.Len(t, resp.Sessions, 1)
assert.Equal(t, "sess-3", resp.Sessions[0].ID)
})
t.Run("multiple independent sessions", func(t *testing.T) {
// Three independent sessions, all are leaves
now := time.Now()
sessions := []session.Info{
{
ID: "sess-1",
ParentSessionID: "",
LastActivityAt: now.Add(-3 * time.Hour),
},
{
ID: "sess-2",
ParentSessionID: "",
LastActivityAt: now.Add(-1 * time.Hour),
},
{
ID: "sess-3",
ParentSessionID: "",
LastActivityAt: now.Add(-2 * time.Hour),
},
}
mockManager.EXPECT().
ListSessions().
Return(sessions)
result, err := handlers.HandleGetSessionLeaves(context.Background(), nil)
require.NoError(t, err)
resp, ok := result.(*GetSessionLeavesResponse)
require.True(t, ok)
assert.Len(t, resp.Sessions, 3)
// Should be sorted by last activity (newest first)
assert.Equal(t, "sess-2", resp.Sessions[0].ID)
assert.Equal(t, "sess-3", resp.Sessions[1].ID)
assert.Equal(t, "sess-1", resp.Sessions[2].ID)
})
t.Run("session with multiple children (fork)", func(t *testing.T) {
// A has two children B and C, should return B and C
now := time.Now()
sessions := []session.Info{
{
ID: "sess-1",
ParentSessionID: "",
LastActivityAt: now.Add(-3 * time.Hour),
},
{
ID: "sess-2",
ParentSessionID: "sess-1",
LastActivityAt: now.Add(-2 * time.Hour),
},
{
ID: "sess-3",
ParentSessionID: "sess-1",
LastActivityAt: now.Add(-1 * time.Hour),
},
}
mockManager.EXPECT().
ListSessions().
Return(sessions)
result, err := handlers.HandleGetSessionLeaves(context.Background(), nil)
require.NoError(t, err)
resp, ok := result.(*GetSessionLeavesResponse)
require.True(t, ok)
assert.Len(t, resp.Sessions, 2)
// Should be sorted by last activity (newest first)
assert.Equal(t, "sess-3", resp.Sessions[0].ID)
assert.Equal(t, "sess-2", resp.Sessions[1].ID)
})
t.Run("deep tree structure", func(t *testing.T) {
// Complex tree:
// A
// / \
// B C
// / \
// D E
// \
// F
// Should return D and F
now := time.Now()
sessions := []session.Info{
{
ID: "sess-A",
ParentSessionID: "",
LastActivityAt: now.Add(-6 * time.Hour),
},
{
ID: "sess-B",
ParentSessionID: "sess-A",
LastActivityAt: now.Add(-5 * time.Hour),
},
{
ID: "sess-C",
ParentSessionID: "sess-A",
LastActivityAt: now.Add(-4 * time.Hour),
},
{
ID: "sess-D",
ParentSessionID: "sess-B",
LastActivityAt: now.Add(-3 * time.Hour),
},
{
ID: "sess-E",
ParentSessionID: "sess-C",
LastActivityAt: now.Add(-2 * time.Hour),
},
{
ID: "sess-F",
ParentSessionID: "sess-E",
LastActivityAt: now.Add(-1 * time.Hour),
},
}
mockManager.EXPECT().
ListSessions().
Return(sessions)
result, err := handlers.HandleGetSessionLeaves(context.Background(), nil)
require.NoError(t, err)
resp, ok := result.(*GetSessionLeavesResponse)
require.True(t, ok)
assert.Len(t, resp.Sessions, 2)
// Should be sorted by last activity (newest first)
assert.Equal(t, "sess-F", resp.Sessions[0].ID)
assert.Equal(t, "sess-D", resp.Sessions[1].ID)
})
t.Run("with request parameters", func(t *testing.T) {
// Test that request parameters are properly parsed (even though empty for now)
sessions := []session.Info{
{
ID: "sess-1",
ParentSessionID: "",
LastActivityAt: time.Now(),
},
}
mockManager.EXPECT().
ListSessions().
Return(sessions)
req := GetSessionLeavesRequest{}
reqJSON, _ := json.Marshal(req)
result, err := handlers.HandleGetSessionLeaves(context.Background(), reqJSON)
require.NoError(t, err)
resp, ok := result.(*GetSessionLeavesResponse)
require.True(t, ok)
assert.Len(t, resp.Sessions, 1)
})
}
func TestHandleInterruptSession(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

View File

@@ -104,6 +104,7 @@ func (s *SQLiteStore) initSchema() error {
CREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
CREATE INDEX IF NOT EXISTS idx_sessions_run_id ON sessions(run_id);
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
-- Single conversation events table
CREATE TABLE IF NOT EXISTS conversation_events (
@@ -323,6 +324,30 @@ func (s *SQLiteStore) applyMigrations() error {
slog.Info("Migration 4 applied successfully")
}
// Migration 5: Add index on parent_session_id for efficient tree queries
if currentVersion < 5 {
slog.Info("Applying migration 5: Add index on parent_session_id")
// Create index on parent_session_id for efficient child queries
_, err = s.db.Exec(`
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)
`)
if err != nil {
return fmt.Errorf("failed to create parent_session_id index: %w", err)
}
// Record migration
_, err = s.db.Exec(`
INSERT INTO schema_version (version, description)
VALUES (5, 'Add index on parent_session_id for efficient tree queries')
`)
if err != nil {
return fmt.Errorf("failed to record migration 5: %w", err)
}
slog.Info("Migration 5 applied successfully")
}
return nil
}

View File

@@ -7,19 +7,18 @@ import (
"context"
"database/sql"
"os"
"path/filepath"
"sync"
"testing"
"time"
claudecode "github.com/humanlayer/humanlayer/claudecode-go"
"github.com/humanlayer/humanlayer/hld/internal/testutil"
)
// TestSQLiteStoreIntegration tests the SQLite store with real database operations
func TestSQLiteStoreIntegration(t *testing.T) {
// Create temporary database
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
dbPath := testutil.DatabasePath(t, "sqlite-integration")
// Create store
store, err := NewSQLiteStore(dbPath)
@@ -565,8 +564,7 @@ func TestSQLiteStoreIntegration(t *testing.T) {
// TestSQLiteStorePersistence verifies data persists across store instances
func TestSQLiteStorePersistence(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "persist.db")
dbPath := testutil.DatabasePath(t, "sqlite-persist")
ctx := context.Background()
// Create first store instance
@@ -641,8 +639,7 @@ func TestSQLiteStorePersistence(t *testing.T) {
// TestSQLiteStoreWithMockSession tests store integration with mock claude sessions
func TestSQLiteStoreWithMockSession(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "mock_session.db")
dbPath := testutil.DatabasePath(t, "sqlite-mock")
ctx := context.Background()
store, err := NewSQLiteStore(dbPath)

View File

@@ -2,22 +2,17 @@ package store
import (
"context"
"os"
"path/filepath"
"testing"
"time"
claudecode "github.com/humanlayer/humanlayer/claudecode-go"
"github.com/humanlayer/humanlayer/hld/internal/testutil"
"github.com/stretchr/testify/require"
)
func TestSQLiteStore(t *testing.T) {
// Create temp database
tmpDir, err := os.MkdirTemp("", "hld-test-*")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
dbPath := filepath.Join(tmpDir, "test.db")
dbPath := testutil.DatabasePath(t, "sqlite")
store, err := NewSQLiteStore(dbPath)
require.NoError(t, err)
defer func() { _ = store.Close() }()
@@ -265,11 +260,7 @@ func TestSQLiteStore(t *testing.T) {
func TestGetSessionConversationWithParentChain(t *testing.T) {
// Create temp database
tmpDir, err := os.MkdirTemp("", "hld-test-parent-*")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
dbPath := filepath.Join(tmpDir, "test.db")
dbPath := testutil.DatabasePath(t, "sqlite-parent")
store, err := NewSQLiteStore(dbPath)
require.NoError(t, err)
defer func() { _ = store.Close() }()

28
humanlayer-tui/CLAUDE.md Normal file
View File

@@ -0,0 +1,28 @@
# CLAUDE.md - HumanLayer TUI
## IMPORTANT: This project is archived
The HumanLayer TUI (Terminal User Interface) is **archived** and no longer actively developed.
### Key Points:
- **No new features** will be added to the TUI
- **No modifications** should be made unless fixing critical bugs
- **Reference only**: The code can be used as reference when implementing features in other components
- **Focus on WUI**: All new UI features should be implemented in the WUI (Web UI) instead
### What This Means for Development:
- When implementing new JSON-RPC endpoints or features, **do not update the TUI**
- When creating implementation plans, **exclude TUI from the scope**
- If you need to understand how something works, you may reference TUI code
- The TUI binary is still built and distributed, but only for legacy support
### Why Archived:
The team has decided to focus development efforts on the WUI (Web UI) which provides a richer user experience and is easier to maintain and extend.
### Current State:
- The TUI remains functional with existing features
- It connects to the hld daemon via JSON-RPC
- Provides session management and approval handling
- Built with Go and Bubble Tea framework
If you have questions about this decision, please consult with the team.

View File

@@ -17,6 +17,7 @@ pub trait DaemonClientTrait: Send + Sync {
async fn health(&self) -> Result<HealthCheckResponse>;
async fn launch_session(&self, req: LaunchSessionRequest) -> Result<LaunchSessionResponse>;
async fn list_sessions(&self) -> Result<ListSessionsResponse>;
async fn get_session_leaves(&self) -> Result<GetSessionLeavesResponse>;
async fn continue_session(&self, req: ContinueSessionRequest) -> Result<ContinueSessionResponse>;
async fn get_session_state(&self, session_id: &str) -> Result<GetSessionStateResponse>;
async fn get_conversation(&self, session_id: Option<&str>, claude_session_id: Option<&str>) -> Result<GetConversationResponse>;
@@ -131,6 +132,10 @@ impl DaemonClientTrait for DaemonClient {
self.send_rpc_request("listSessions", None::<()>).await
}
async fn get_session_leaves(&self) -> Result<GetSessionLeavesResponse> {
self.send_rpc_request("getSessionLeaves", None::<()>).await
}
async fn continue_session(&self, req: ContinueSessionRequest) -> Result<ContinueSessionResponse> {
self.send_rpc_request("continueSession", Some(req)).await
}

View File

@@ -94,6 +94,14 @@ pub struct ListSessionsResponse {
pub sessions: Vec<SessionInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetSessionLeavesRequest {}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetSessionLeavesResponse {
pub sessions: Vec<SessionInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionInfo {
pub id: String,

View File

@@ -72,6 +72,21 @@ async fn list_sessions(
}
}
#[tauri::command]
async fn get_session_leaves(
state: State<'_, AppState>,
) -> std::result::Result<daemon_client::GetSessionLeavesResponse, String> {
let client_guard = state.client.lock().await;
match &*client_guard {
Some(client) => client
.get_session_leaves()
.await
.map_err(|e| e.to_string()),
None => Err("Not connected to daemon".to_string()),
}
}
#[tauri::command]
async fn get_session_state(
state: State<'_, AppState>,
@@ -306,6 +321,7 @@ pub fn run() {
daemon_health,
launch_session,
list_sessions,
get_session_leaves,
get_session_state,
continue_session,
get_conversation,

View File

@@ -38,7 +38,7 @@ export const useStore = create<StoreState>((set, get) => ({
})),
refreshSessions: async () => {
try {
const response = await daemonClient.listSessions()
const response = await daemonClient.getSessionLeaves()
set({ sessions: response.sessions })
} catch (error) {
console.error('Failed to refresh sessions:', error)

View File

@@ -22,7 +22,7 @@ export function useSessions(): UseSessionsReturn {
setLoading(true)
setError(null)
const response = await daemonClient.listSessions()
const response = await daemonClient.getSessionLeaves()
// Transform to UI-friendly format
const summaries: SessionSummary[] = response.sessions.map(session => ({

View File

@@ -5,6 +5,7 @@ import type {
LaunchSessionRequest,
LaunchSessionResponse,
ListSessionsResponse,
GetSessionLeavesResponse,
GetSessionStateResponse,
ContinueSessionRequest,
ContinueSessionResponse,
@@ -31,6 +32,10 @@ export class DaemonClient {
return await invoke('list_sessions')
}
async getSessionLeaves(): Promise<GetSessionLeavesResponse> {
return await invoke('get_session_leaves')
}
async getSessionState(sessionId: string): Promise<GetSessionStateResponse> {
return await invoke('get_session_state', { sessionId })
}

View File

@@ -106,6 +106,14 @@ export interface ListSessionsResponse {
sessions: SessionInfo[]
}
export interface GetSessionLeavesRequest {
// Empty for now - could add filters later
}
export interface GetSessionLeavesResponse {
sessions: SessionInfo[]
}
// Contact channel types
export interface SlackChannel {
channel_or_user_id: string