Claude Code emits many progress lines during work, burying the last assistant text. 5 lines was not enough to reach it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
98 lines
2.3 KiB
Go
98 lines
2.3 KiB
Go
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
|
|
}
|