- TailReadJSONL: reverse-read JSONL without loading entire file - FindSessionForProcess: match PID to most recent JSONL via EncodePath - DetectState: heuristic based on last message stop_reason + tool name - ExtractPreview: extract first 3 lines of last assistant text - All 17 tests pass (session_test.go + state_test.go) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
1.8 KiB
Go
84 lines
1.8 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const IdleThreshold = 60 * 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
|
|
}
|
|
}
|
|
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
|
|
// 3 lines, truncated to 200 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 ""
|
|
}
|
|
|
|
func truncatePreview(text string) string {
|
|
lines := strings.SplitN(text, "\n", 4)
|
|
if len(lines) > 3 {
|
|
lines = lines[:3]
|
|
}
|
|
result := strings.Join(lines, "\n")
|
|
|
|
if len(result) > 200 {
|
|
result = result[:200] + "..."
|
|
}
|
|
return result
|
|
}
|