diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index 0a49f5b..18f2bf4 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -228,6 +228,11 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { formatMessage := func(message string, userInput string) string { return mf.FormatAgentMessage(config.AgentType, message, userInput) } + + isAgentReadyForInitialPrompt := func(message string) bool { + return mf.IsAgentReadyForInitialPrompt(config.AgentType, message) + } + conversation := st.NewConversation(ctx, st.ConversationConfig{ AgentType: config.AgentType, AgentIO: config.Process, @@ -237,6 +242,7 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { SnapshotInterval: snapshotInterval, ScreenStabilityLength: 2 * time.Second, FormatMessage: formatMessage, + ReadyForInitialPrompt: isAgentReadyForInitialPrompt, }, config.InitialPrompt) emitter := NewEventEmitter(1024) @@ -331,6 +337,7 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) { s.logger.Error("Failed to send initial prompt", "error", err) } else { s.conversation.InitialPromptSent = true + s.conversation.ReadyForInitialPrompt = false currentStatus = st.ConversationStatusChanging s.logger.Info("Initial prompt sent successfully") } diff --git a/lib/msgfmt/agent_readiness.go b/lib/msgfmt/agent_readiness.go new file mode 100644 index 0000000..c814685 --- /dev/null +++ b/lib/msgfmt/agent_readiness.go @@ -0,0 +1,50 @@ +package msgfmt + +func IsAgentReadyForInitialPrompt(agentType AgentType, message string) bool { + switch agentType { + case AgentTypeClaude: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeGoose: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeAider: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeCodex: + return isCodexAgentReadyForInitialPrompt(message) + case AgentTypeGemini: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeCopilot: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeAmp: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeCursor: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeAuggie: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeAmazonQ: + return isGenericAgentReadyForInitialPrompt(message) + case AgentTypeOpencode: + return isOpencodeAgentReadyForInitialPrompt(message) + case AgentTypeCustom: + return isGenericAgentReadyForInitialPrompt(message) + default: + return true + } +} + +func isGenericAgentReadyForInitialPrompt(message string) bool { + message = trimEmptyLines(message) + messageWithoutInputBox := removeMessageBox(message) + return len(messageWithoutInputBox) != len(message) +} + +func isOpencodeAgentReadyForInitialPrompt(message string) bool { + message = trimEmptyLines(message) + messageWithoutInputBox := removeOpencodeMessageBox(message) + return len(messageWithoutInputBox) != len(message) +} + +func isCodexAgentReadyForInitialPrompt(message string) bool { + message = trimEmptyLines(message) + messageWithoutInputBox := removeCodexInputBox(message) + return len(messageWithoutInputBox) != len(message) +} diff --git a/lib/msgfmt/agent_readiness_test.go b/lib/msgfmt/agent_readiness_test.go new file mode 100644 index 0000000..9576333 --- /dev/null +++ b/lib/msgfmt/agent_readiness_test.go @@ -0,0 +1,56 @@ +package msgfmt + +import ( + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsAgentReadyForInitialPrompt(t *testing.T) { + dir := "testdata/initialization" + agentTypes := []AgentType{AgentTypeClaude, AgentTypeGoose, AgentTypeAider, AgentTypeGemini, AgentTypeCopilot, AgentTypeAmp, AgentTypeCodex, AgentTypeCursor, AgentTypeAuggie, AgentTypeAmazonQ, AgentTypeOpencode} + for _, agentType := range agentTypes { + t.Run(string(agentType), func(t *testing.T) { + t.Run("ready", func(t *testing.T) { + cases, err := testdataDir.ReadDir(path.Join(dir, string(agentType), "ready")) + if err != nil { + t.Errorf("failed to read ready cases for agent type %s: %s", agentType, err) + } + if len(cases) == 0 { + t.Errorf("no ready cases found for agent type %s", agentType) + } + for _, c := range cases { + if c.IsDir() { + continue + } + t.Run(c.Name(), func(t *testing.T) { + msg, err := testdataDir.ReadFile(path.Join(dir, string(agentType), "ready", c.Name())) + assert.NoError(t, err) + assert.True(t, IsAgentReadyForInitialPrompt(agentType, string(msg)), "Expected agent to be ready for message:\n%s", string(msg)) + }) + } + }) + + t.Run("not_ready", func(t *testing.T) { + cases, err := testdataDir.ReadDir(path.Join(dir, string(agentType), "not_ready")) + if err != nil { + t.Errorf("failed to read not_ready cases for agent type %s: %s", agentType, err) + } + if len(cases) == 0 { + t.Errorf("no not_ready cases found for agent type %s", agentType) + } + for _, c := range cases { + if c.IsDir() { + continue + } + t.Run(c.Name(), func(t *testing.T) { + msg, err := testdataDir.ReadFile(path.Join(dir, string(agentType), "not_ready", c.Name())) + assert.NoError(t, err) + assert.False(t, IsAgentReadyForInitialPrompt(agentType, string(msg)), "Expected agent to not be ready for message:\n%s", string(msg)) + }) + } + }) + }) + } +} diff --git a/lib/msgfmt/testdata/initialization/aider/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/aider/not_ready/msg.txt new file mode 100644 index 0000000..be53715 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/aider/not_ready/msg.txt @@ -0,0 +1,8 @@ +──────────────────────────────────────────────────────────────────────────────── +Aider v0.81.1 +Main model: anthropic/claude-3-7-sonnet-20250219 with diff edit format, infinite +output +Weak model: anthropic/claude-3-5-haiku-20241022 +Git repo: .git with 47 files +Repo-map: using 4096 tokens, auto refresh +──────────────────────────────────────────────────────────────────────────────── diff --git a/lib/msgfmt/testdata/initialization/aider/ready/msg.txt b/lib/msgfmt/testdata/initialization/aider/ready/msg.txt new file mode 100644 index 0000000..e963a1b --- /dev/null +++ b/lib/msgfmt/testdata/initialization/aider/ready/msg.txt @@ -0,0 +1,9 @@ +──────────────────────────────────────────────────────────────────────────────── +Aider v0.81.1 +Main model: anthropic/claude-3-7-sonnet-20250219 with diff edit format, infinite +output +Weak model: anthropic/claude-3-5-haiku-20241022 +Git repo: .git with 47 files +Repo-map: using 4096 tokens, auto refresh +──────────────────────────────────────────────────────────────────────────────── +> \ No newline at end of file diff --git a/lib/msgfmt/testdata/initialization/amazonq/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/amazonq/not_ready/msg.txt new file mode 100644 index 0000000..dfcc1ff --- /dev/null +++ b/lib/msgfmt/testdata/initialization/amazonq/not_ready/msg.txt @@ -0,0 +1,12 @@ +✓ coder loaded in 0.03 s + +Welcome to Amazon Q! + +💡 Run /prompts to learn how to build & run repeatable workflows + +/help all commands +ctrl + j new lines +ctrl + s fuzzy search + + +🤖 You are chatting with claude-sonnet-4 diff --git a/lib/msgfmt/testdata/initialization/amazonq/ready/msg.txt b/lib/msgfmt/testdata/initialization/amazonq/ready/msg.txt new file mode 100644 index 0000000..4f43700 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/amazonq/ready/msg.txt @@ -0,0 +1,14 @@ +✓ coder loaded in 0.03 s + +Welcome to Amazon Q! + +💡 Run /prompts to learn how to build & run repeatable workflows + +/help all commands +ctrl + j new lines +ctrl + s fuzzy search + + +🤖 You are chatting with claude-sonnet-4 + +> \ No newline at end of file diff --git a/lib/msgfmt/testdata/initialization/amp/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/amp/not_ready/msg.txt new file mode 100644 index 0000000..61f23dc --- /dev/null +++ b/lib/msgfmt/testdata/initialization/amp/not_ready/msg.txt @@ -0,0 +1,22 @@ + + + ............ + ..:::-------::::... + ..::--=========---:::... Welcome to Amp + .::-===++++++++===---:::.. + ..:--==+++******+++===--:::... + .:--=+++**********++===--:::... Type / to use slash commands + .::-==++***#####****++==---:::.. Type @ to mention files + .:--=++****#####****++===---::... Ctrl+C to exit + .:--=+++****###****+++===---:::.. + .:--==+++*********++++===---:::.. + ..:--==++++*****++++====----:::.. /help for more + .::--===+++++++++====-----:::... + .::---===========------::::... + ..:::-------------:::::::... amp -x "Run the linter and fix the errors" + ...::::::::::::::::..... + .................. + + + + diff --git a/lib/msgfmt/testdata/initialization/amp/ready/msg.txt b/lib/msgfmt/testdata/initialization/amp/ready/msg.txt new file mode 100644 index 0000000..a3be16b --- /dev/null +++ b/lib/msgfmt/testdata/initialization/amp/ready/msg.txt @@ -0,0 +1,25 @@ + + + ............ + ..:::-------::::... + ..::--=========---:::... Welcome to Amp + .::-===++++++++===---:::.. + ..:--==+++******+++===--:::... + .:--=+++**********++===--:::... Type / to use slash commands + .::-==++***#####****++==---:::.. Type @ to mention files + .:--=++****#####****++===---::... Ctrl+C to exit + .:--=+++****###****+++===---:::.. + .:--==+++*********++++===---:::.. + ..:--==++++*****++++====----:::.. /help for more + .::--===+++++++++====-----:::... + .::---===========------::::... + ..:::-------------:::::::... amp -x "Run the linter and fix the errors" + ...::::::::::::::::..... + .................. + + + + +─────────────── +> +─────────────── diff --git a/lib/msgfmt/testdata/initialization/auggie/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/auggie/not_ready/msg.txt new file mode 100644 index 0000000..902ff9e --- /dev/null +++ b/lib/msgfmt/testdata/initialization/auggie/not_ready/msg.txt @@ -0,0 +1,12 @@ + + + Getting started with Auggie by Augment Code + 1. You can ask questions, edit files, or run commands + 2. Your workspace is automatically indexed for best results + 3. Commands will run automatically + + + 💡 For automation, use 'auggie --print "your task"' + + + diff --git a/lib/msgfmt/testdata/initialization/auggie/ready/msg.txt b/lib/msgfmt/testdata/initialization/auggie/ready/msg.txt new file mode 100644 index 0000000..b5751da --- /dev/null +++ b/lib/msgfmt/testdata/initialization/auggie/ready/msg.txt @@ -0,0 +1,19 @@ + + + Getting started with Auggie by Augment Code + 1. You can ask questions, edit files, or run commands + 2. Your workspace is automatically indexed for best results + 3. Commands will run automatically + + + 💡 For automation, use 'auggie --print "your task"' + + + + +╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ › Try 'how do I log an error?' or type / for commands │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + Type / for commands • ↑↓ for history • Esc/Ctrl+C to interrupt ~/Documents/work/agentapi + + diff --git a/lib/msgfmt/testdata/initialization/claude/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/claude/not_ready/msg.txt new file mode 100644 index 0000000..9d39e34 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/claude/not_ready/msg.txt @@ -0,0 +1,14 @@ +╭────────────────────────────────────────────╮ +│ ✻ Welcome to Claude Code research preview! │ +│ │ +│ /help for help │ +│ │ +│ cwd: /Users/hugodutka/dev/agentapi │ +╰────────────────────────────────────────────╯ + + Tips for getting started: + + 1. Run /init to create a CLAUDE.md file with instructions for Claude + 2. Run /terminal-setup to set up terminal integration + 3. Use Claude to help with file analysis, editing, bash commands and git + 4. Be as specific as you would with another engineer for the best results diff --git a/lib/msgfmt/testdata/initialization/claude/ready/msg.txt b/lib/msgfmt/testdata/initialization/claude/ready/msg.txt new file mode 100644 index 0000000..d1a9bb0 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/claude/ready/msg.txt @@ -0,0 +1,19 @@ +╭────────────────────────────────────────────╮ +│ ✻ Welcome to Claude Code research preview! │ +│ │ +│ /help for help │ +│ │ +│ cwd: /Users/hugodutka/dev/agentapi │ +╰────────────────────────────────────────────╯ + + Tips for getting started: + + 1. Run /init to create a CLAUDE.md file with instructions for Claude + 2. Run /terminal-setup to set up terminal integration + 3. Use Claude to help with file analysis, editing, bash commands and git + 4. Be as specific as you would with another engineer for the best results + +╭──────────────────────────────────────────────────────────────────────────────╮ +│ > Try "refactor handler.go" │ +╰──────────────────────────────────────────────────────────────────────────────╯ + ? for shortcuts \ No newline at end of file diff --git a/lib/msgfmt/testdata/initialization/codex/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/codex/not_ready/msg.txt new file mode 100644 index 0000000..7a50888 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/codex/not_ready/msg.txt @@ -0,0 +1,8 @@ +>_ You are using OpenAI Codex in ~ + + To get started, describe a task or try one of these commands: + + /init - create an AGENTS.md file with instructions for Codex + /status - show current session configuration and token usage + /diff - show git diff (including untracked files) + /prompts - show example prompts diff --git a/lib/msgfmt/testdata/initialization/codex/ready/msg.txt b/lib/msgfmt/testdata/initialization/codex/ready/msg.txt new file mode 100644 index 0000000..76b1ef4 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/codex/ready/msg.txt @@ -0,0 +1,11 @@ +>_ You are using OpenAI Codex in ~ + + To get started, describe a task or try one of these commands: + + /init - create an AGENTS.md file with instructions for Codex + /status - show current session configuration and token usage + /diff - show git diff (including untracked files) + /prompts - show example prompts + +▌ Ask Codex to do anything + ⏎ send Shift+⏎ newline Ctrl+C quit \ No newline at end of file diff --git a/lib/msgfmt/testdata/initialization/copilot/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/copilot/not_ready/msg.txt new file mode 100644 index 0000000..c84773f --- /dev/null +++ b/lib/msgfmt/testdata/initialization/copilot/not_ready/msg.txt @@ -0,0 +1,11 @@ + Welcome to GitHub Copilot CLI + Version 0.0.328 · Commit 3755a93 + + Copilot can write, test and debug code right from your terminal. Describe a + task to get started or enter ? for help. Copilot uses AI, check for mistakes. + + ● Logged in with gh as user: 35C4n0r + + ● Connected to GitHub MCP Server + + ~/Documents/work/agentapi [⎇ feat-github-cli*] diff --git a/lib/msgfmt/testdata/initialization/copilot/ready/msg.txt b/lib/msgfmt/testdata/initialization/copilot/ready/msg.txt new file mode 100644 index 0000000..dee0c4d --- /dev/null +++ b/lib/msgfmt/testdata/initialization/copilot/ready/msg.txt @@ -0,0 +1,15 @@ + Welcome to GitHub Copilot CLI + Version 0.0.328 · Commit 3755a93 + + Copilot can write, test and debug code right from your terminal. Describe a + task to get started or enter ? for help. Copilot uses AI, check for mistakes. + + ● Logged in with gh as user: 35C4n0r + + ● Connected to GitHub MCP Server + + ~/Documents/work/agentapi [⎇ feat-github-cli*] + ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ > Enter @ to mention files or / for commands │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + Ctrl+c Exit · Ctrl+r Expand all \ No newline at end of file diff --git a/lib/msgfmt/testdata/initialization/cursor/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/cursor/not_ready/msg.txt new file mode 100644 index 0000000..5857fbb --- /dev/null +++ b/lib/msgfmt/testdata/initialization/cursor/not_ready/msg.txt @@ -0,0 +1,11 @@ + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ⬢ Welcome to Cursor Agent Beta │ + │ │ + │ Cursor Agent CLI is in beta. Security safeguards are still evolving. It can read, modify, and delete files, and execute shell commands you approve. Use at your own risk and only in trusted environments. │ + │ │ + │ Please read about our security at https://cursor.com/security. │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + Cursor Agent + ~/Documents/work/agentapi · feat-cursor-cli diff --git a/lib/msgfmt/testdata/initialization/cursor/ready/msg.txt b/lib/msgfmt/testdata/initialization/cursor/ready/msg.txt new file mode 100644 index 0000000..c560cda --- /dev/null +++ b/lib/msgfmt/testdata/initialization/cursor/ready/msg.txt @@ -0,0 +1,19 @@ + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ⬢ Welcome to Cursor Agent Beta │ + │ │ + │ Cursor Agent CLI is in beta. Security safeguards are still evolving. It can read, modify, and delete files, and execute shell commands you approve. Use at your own risk and only in trusted environments. │ + │ │ + │ Please read about our security at https://cursor.com/security. │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + Cursor Agent + ~/Documents/work/agentapi · feat-cursor-cli + + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ → Plan, search, build anything │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + + OpenAI GPT-5 + / for commands · @ for files + diff --git a/lib/msgfmt/testdata/initialization/gemini/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/gemini/not_ready/msg.txt new file mode 100644 index 0000000..375f12e --- /dev/null +++ b/lib/msgfmt/testdata/initialization/gemini/not_ready/msg.txt @@ -0,0 +1,7 @@ +Tips for getting started: +1. Ask questions, edit files, or run commands. +2. Be specific for the best results. +3. Create GEMINI.md files to customize your interactions with Gemini. +4. /help for more information. + + diff --git a/lib/msgfmt/testdata/initialization/gemini/ready/msg.txt b/lib/msgfmt/testdata/initialization/gemini/ready/msg.txt new file mode 100644 index 0000000..2cad627 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/gemini/ready/msg.txt @@ -0,0 +1,13 @@ +Tips for getting started: +1. Ask questions, edit files, or run commands. +2. Be specific for the best results. +3. Create GEMINI.md files to customize your interactions with Gemini. +4. /help for more information. + + + +╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ > Type your message or @path/to/file │ +╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + +~/Documents/work/agentapi (feat-claude-cli*) no sandbox (see /docs) gemini-2.5-pro (100% context left) diff --git a/lib/msgfmt/testdata/initialization/goose/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/goose/not_ready/msg.txt new file mode 100644 index 0000000..7d49347 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/goose/not_ready/msg.txt @@ -0,0 +1,6 @@ +starting session | provider: anthropic model: claude-3-5-sonnet-latest + logging to /Users/hugodutka/.local/share/goose/sessions/20250409_131437.json +l + working directory: /Users/hugodutka/dev/agentapi + +Goose is running! Enter your instructions, or try asking what goose can do. diff --git a/lib/msgfmt/testdata/initialization/goose/ready/msg.txt b/lib/msgfmt/testdata/initialization/goose/ready/msg.txt new file mode 100644 index 0000000..196b210 --- /dev/null +++ b/lib/msgfmt/testdata/initialization/goose/ready/msg.txt @@ -0,0 +1,8 @@ +starting session | provider: anthropic model: claude-3-5-sonnet-latest + logging to /Users/hugodutka/.local/share/goose/sessions/20250409_131437.json +l + working directory: /Users/hugodutka/dev/agentapi + +Goose is running! Enter your instructions, or try asking what goose can do. + +( O)> \ No newline at end of file diff --git a/lib/msgfmt/testdata/initialization/opencode/not_ready/msg.txt b/lib/msgfmt/testdata/initialization/opencode/not_ready/msg.txt new file mode 100644 index 0000000..21945ef --- /dev/null +++ b/lib/msgfmt/testdata/initialization/opencode/not_ready/msg.txt @@ -0,0 +1,12 @@ + █▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀ + █░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀ + ▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀ + v0.6.8 + + /new new session ctrl+x n + /help show help ctrl+x h + /share share session ctrl+x s + /models list models ctrl+x m + + + Grok Code is free for a limited time diff --git a/lib/msgfmt/testdata/initialization/opencode/ready/msg.txt b/lib/msgfmt/testdata/initialization/opencode/ready/msg.txt new file mode 100644 index 0000000..25ae98e --- /dev/null +++ b/lib/msgfmt/testdata/initialization/opencode/ready/msg.txt @@ -0,0 +1,21 @@ + █▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀ + █░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀ + ▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀ + v0.6.8 + + /new new session ctrl+x n + /help show help ctrl+x h + /share share session ctrl+x s + /models list models ctrl+x m + + + Grok Code is free for a limited time + + + ┃ ┃ + ┃ > ┃ + ┃ ┃ + enter send + + + diff --git a/lib/screentracker/conversation.go b/lib/screentracker/conversation.go index 4617e8e..043f440 100644 --- a/lib/screentracker/conversation.go +++ b/lib/screentracker/conversation.go @@ -41,6 +41,8 @@ type ConversationConfig struct { // SkipSendMessageStatusCheck skips the check for whether the message can be sent. // This is used in tests SkipSendMessageStatusCheck bool + // ReadyForInitialPrompt detects whether the agent has initialized and is ready to accept the initial prompt + ReadyForInitialPrompt func(message string) bool } type ConversationRole string @@ -78,6 +80,8 @@ type Conversation struct { InitialPrompt string // InitialPromptSent keeps track if the InitialPrompt has been successfully sent to the agents InitialPromptSent bool + // ReadyForInitialPrompt keeps track if the agent is ready to accept the initial prompt + ReadyForInitialPrompt bool } type ConversationStatus string @@ -401,6 +405,15 @@ func (c *Conversation) statusInner() ConversationStatus { return ConversationStatusChanging } } + + if !c.InitialPromptSent && !c.ReadyForInitialPrompt { + if len(snapshots) > 0 && c.cfg.ReadyForInitialPrompt(snapshots[len(snapshots)-1].screen) { + c.ReadyForInitialPrompt = true + return ConversationStatusStable + } + return ConversationStatusChanging + } + return ConversationStatusStable } diff --git a/lib/screentracker/conversation_test.go b/lib/screentracker/conversation_test.go index 92fe5ac..f77d114 100644 --- a/lib/screentracker/conversation_test.go +++ b/lib/screentracker/conversation_test.go @@ -406,3 +406,94 @@ func TestPartsToString(t *testing.T) { ), ) } + +func TestInitialPromptReadiness(t *testing.T) { + now := time.Now() + + t.Run("agent not ready - status remains changing", func(t *testing.T) { + cfg := st.ConversationConfig{ + GetTime: func() time.Time { return now }, + SnapshotInterval: 1 * time.Second, + ScreenStabilityLength: 0, + AgentIO: &testAgent{screen: "loading..."}, + ReadyForInitialPrompt: func(message string) bool { + return message == "ready" + }, + } + c := st.NewConversation(context.Background(), cfg, "initial prompt here") + + // Fill buffer with stable snapshots, but agent is not ready + c.AddSnapshot("loading...") + + // Even though screen is stable, status should be changing because agent is not ready + assert.Equal(t, st.ConversationStatusChanging, c.Status()) + assert.False(t, c.ReadyForInitialPrompt) + assert.False(t, c.InitialPromptSent) + }) + + t.Run("agent becomes ready - status changes to stable", func(t *testing.T) { + cfg := st.ConversationConfig{ + GetTime: func() time.Time { return now }, + SnapshotInterval: 1 * time.Second, + ScreenStabilityLength: 0, + AgentIO: &testAgent{screen: "loading..."}, + ReadyForInitialPrompt: func(message string) bool { + return message == "ready" + }, + } + c := st.NewConversation(context.Background(), cfg, "initial prompt here") + + // Agent not ready initially + c.AddSnapshot("loading...") + assert.Equal(t, st.ConversationStatusChanging, c.Status()) + + // Agent becomes ready + c.AddSnapshot("ready") + assert.Equal(t, st.ConversationStatusStable, c.Status()) + assert.True(t, c.ReadyForInitialPrompt) + assert.False(t, c.InitialPromptSent) + }) + + t.Run("no initial prompt - normal status logic applies", func(t *testing.T) { + cfg := st.ConversationConfig{ + GetTime: func() time.Time { return now }, + SnapshotInterval: 1 * time.Second, + ScreenStabilityLength: 0, + AgentIO: &testAgent{screen: "loading..."}, + ReadyForInitialPrompt: func(message string) bool { + return false // Agent never ready + }, + } + // Empty initial prompt means no need to wait for readiness + c := st.NewConversation(context.Background(), cfg, "") + + c.AddSnapshot("loading...") + + // Status should be stable because no initial prompt to wait for + assert.Equal(t, st.ConversationStatusStable, c.Status()) + assert.False(t, c.ReadyForInitialPrompt) + assert.True(t, c.InitialPromptSent) // Set to true when initial prompt is empty + }) + + t.Run("initial prompt sent - normal status logic applies", func(t *testing.T) { + cfg := st.ConversationConfig{ + GetTime: func() time.Time { return now }, + SnapshotInterval: 1 * time.Second, + ScreenStabilityLength: 0, + AgentIO: &testAgent{screen: "processing..."}, + ReadyForInitialPrompt: func(message string) bool { + return false // Agent never ready + }, + } + c := st.NewConversation(context.Background(), cfg, "initial prompt here") + // Manually mark as sent to simulate that initial prompt was already sent + c.InitialPromptSent = true + + c.AddSnapshot("processing...") + + // Status should be stable because initial prompt was already sent + assert.Equal(t, st.ConversationStatusStable, c.Status()) + assert.False(t, c.ReadyForInitialPrompt) + assert.True(t, c.InitialPromptSent) + }) +}