package main import ( "strings" "time" ) const IdleThreshold = 60 * time.Second // PermissionStallThreshold: if a tool_use has been pending this long // without any new JSONL entry, Claude is likely waiting for user approval. const PermissionStallThreshold = 10 * time.Second // DetectState determines the session state from the last JSONL messages. // The now parameter enables deterministic testing of the idle threshold. func DetectState(messages []JSONLMessage, now time.Time) SessionState { if len(messages) == 0 { return Unknown } last := messages[len(messages)-1] // Check idle threshold first if ts, err := time.Parse(time.RFC3339, last.Timestamp); err == nil { if now.Sub(ts) > IdleThreshold { return Idle } } if last.Type == "assistant" && last.Message != nil { switch last.Message.StopReason { case "end_turn": return NeedsInput case "tool_use": for _, block := range last.Message.Content { if block.Type == "tool_use" && block.Name == "AskUserQuestion" { return NeedsInput } } // If tool_use is stale (no new JSONL activity), Claude is likely // waiting for a permission prompt approval. if ts, err := time.Parse(time.RFC3339, last.Timestamp); err == nil { if now.Sub(ts) > PermissionStallThreshold { return NeedsInput } } return Working } } if last.Type == "progress" { return Working } if last.Type == "user" && last.Message != nil { for _, block := range last.Message.Content { if block.Type == "tool_result" { return Working } } } return Unknown } // ExtractPreview finds the last assistant text content and returns // the first 5 lines, truncated to 300 characters. func ExtractPreview(messages []JSONLMessage) string { for i := len(messages) - 1; i >= 0; i-- { msg := messages[i] if msg.Type != "assistant" || msg.Message == nil { continue } for _, block := range msg.Message.Content { if block.Type == "text" && block.Text != "" { return truncatePreview(block.Text) } } } return "" } const previewLines = 5 const previewMaxChars = 300 func truncatePreview(text string) string { lines := strings.SplitN(text, "\n", previewLines+1) if len(lines) > previewLines { lines = lines[:previewLines] } result := strings.Join(lines, "\n") if len(result) > previewMaxChars { result = result[:previewMaxChars] + "..." } return result }