From 2b96c7dd4129a38317040cba3fe7bbb422648407 Mon Sep 17 00:00:00 2001 From: Stanislav Hamara Date: Thu, 15 Jan 2026 15:26:24 +0000 Subject: [PATCH 1/4] AI enqueue implementation --- cagent-recording-1767970646.yaml | 256 ++++++++++++++++++++++++++++ cagent-recording-1767970795.yaml | 190 +++++++++++++++++++++ pkg/tui/components/editor/editor.go | 6 +- pkg/tui/messages/messages.go | 1 + pkg/tui/page/chat/chat.go | 108 +++++++++++- pkg/tui/page/chat/runtime_events.go | 6 +- pkg/tui/tui.go | 13 ++ testFile.yaml | 241 ++++++++++++++++++++++++++ 8 files changed, 814 insertions(+), 7 deletions(-) create mode 100644 cagent-recording-1767970646.yaml create mode 100644 cagent-recording-1767970795.yaml create mode 100644 testFile.yaml diff --git a/cagent-recording-1767970646.yaml b/cagent-recording-1767970646.yaml new file mode 100644 index 000000000..b724105c0 --- /dev/null +++ b/cagent-recording-1767970646.yaml @@ -0,0 +1,256 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.anthropic.com + body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following message a user sent to an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text, nothing else.\n\nUser message: hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given a conversation history and asked to create a title that captures the main topic.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[],"stream":true}' + url: https://api.anthropic.com/v1/messages + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01P91w2kca4k1cXX3Bi7AGHV","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Brief"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Greeting"} } + + event: ping + data: {"type": "ping"} + + event: content_block_stop + data: {"type":"content_block_stop","index":0} + + event: ping + data: {"type": "ping"} + + event: ping + data: {"type": "ping"} + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":6} } + + event: message_stop + data: {"type":"message_stop" } + + headers: {} + status: 200 OK + code: 200 + duration: 691.282667ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.anthropic.com + body: '{"max_tokens":64000,"messages":[{"content":[{"text":"hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are an AI assistant with expertise in the moby/moby project''s documentation.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{},"type":"object"},"name":"fetch_moby_documentation","description":"Fetch entire documentation file from GitHub repository: moby/moby. Useful for general questions. Always call this tool first if asked about moby/moby."},{"input_schema":{"properties":{"query":{"description":"The search query to find relevant documentation","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_documentation","description":"Semantically search within the fetched documentation from GitHub repository: moby/moby. Useful for specific queries."},{"input_schema":{"properties":{"page":{"description":"Page number to retrieve (starting from 1). Each page contains 30 results.","type":"number"},"query":{"description":"The search query to find relevant code files","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_code","description":"Search for code within the GitHub repository: \"moby/moby\" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant."},{"input_schema":{"properties":{"url":{"description":"The URL of the document or page to fetch","type":"string"}},"required":["url"],"type":"object"},"name":"fetch_generic_url_content","description":"Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation."}],"stream":true}' + url: https://api.anthropic.com/v1/messages + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01Nhz3DnX7RNe6x1a9y5Se1S","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}} + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"! I'm an AI"} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" assistant with"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" expertise in the moby"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"/moby project's"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation. The"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" mo"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"by/"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"moby repository"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" is the"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" open"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"-source development"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" repository"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for Docker"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" a platform"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for developing"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":", shipping, and running"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" applications in"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" containers.\n\nIs"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" there something specific about Docker"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" or the moby/"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"moby project that you"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'d like to learn"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" about"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"?"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" I can help you with"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":":"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\n- General information about"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Docker an"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d its"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" components"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Docker"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" installation and configuration"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Docker"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" commands and their"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" usage\n- Docker"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" networking"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" storage, or"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" security"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Docker"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" API"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" an"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d development"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" topics"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- An"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d much more relate"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d to the moby/"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"moby project\n\nFeel"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" free to ask any"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" questions you have,"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and I'll do my"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" best to assist"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0} + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":154} } + + event: message_stop + data: {"type":"message_stop" } + + headers: {} + status: 200 OK + code: 200 + duration: 538.213ms diff --git a/cagent-recording-1767970795.yaml b/cagent-recording-1767970795.yaml new file mode 100644 index 000000000..320f78a67 --- /dev/null +++ b/cagent-recording-1767970795.yaml @@ -0,0 +1,190 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.anthropic.com + body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following message a user sent to an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text, nothing else.\n\nUser message: test","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given a conversation history and asked to create a title that captures the main topic.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[],"stream":true}' + url: https://api.anthropic.com/v1/messages + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_019HzmEffBbC1QRrN5EQiRcF","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Test"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Message"} } + + event: ping + data: {"type": "ping"} + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: ping + data: {"type": "ping"} + + event: ping + data: {"type": "ping"} + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: {} + status: 200 OK + code: 200 + duration: 734.233834ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.anthropic.com + body: '{"max_tokens":64000,"messages":[{"content":[{"text":"test","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are an AI assistant with expertise in the moby/moby project''s documentation.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{},"type":"object"},"name":"fetch_moby_documentation","description":"Fetch entire documentation file from GitHub repository: moby/moby. Useful for general questions. Always call this tool first if asked about moby/moby."},{"input_schema":{"properties":{"query":{"description":"The search query to find relevant documentation","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_documentation","description":"Semantically search within the fetched documentation from GitHub repository: moby/moby. Useful for specific queries."},{"input_schema":{"properties":{"page":{"description":"Page number to retrieve (starting from 1). Each page contains 30 results.","type":"number"},"query":{"description":"The search query to find relevant code files","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_code","description":"Search for code within the GitHub repository: \"moby/moby\" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant."},{"input_schema":{"properties":{"url":{"description":"The URL of the document or page to fetch","type":"string"}},"required":["url"],"type":"object"},"name":"fetch_generic_url_content","description":"Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation."}],"stream":true}' + url: https://api.anthropic.com/v1/messages + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01MU1MMAvMthqVk7azXx2eKz","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d be happy to help you"} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with the"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" moby/moby"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" project. Your"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" message"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" \""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"test\" seems"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" to be a simple"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" test to check"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" if I'm working properly"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":".\n\nTo provide"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with useful information about the"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" moby/moby"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" project ("} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"which is"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Docker Engine"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" core"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"), I can"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" fetch"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" first"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" to"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" give"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you an"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" overview."} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: content_block_start + data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01GLWQA6EprQDqdKGqchE6mE","name":"fetch_moby_documentation","input":{}} } + + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} } + + event: content_block_stop + data: {"type":"content_block_stop","index":1 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":111} } + + event: message_stop + data: {"type":"message_stop" } + + headers: {} + status: 200 OK + code: 200 + duration: 565.07425ms diff --git a/pkg/tui/components/editor/editor.go b/pkg/tui/components/editor/editor.go index 15f78633d..f3995660f 100644 --- a/pkg/tui/components/editor/editor.go +++ b/pkg/tui/components/editor/editor.go @@ -664,7 +664,7 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) { // If plain enter and textarea inserted a newline, submit the previous value if value != prev && msg.String() == "enter" { - if prev != "" && !e.working { + if prev != "" { e.textarea.SetValue(prev) e.textarea.MoveToEnd() cmd := e.resetAndSend(prev) @@ -674,7 +674,7 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) { } // Normal enter submit: send current value - if value != "" && !e.working { + if value != "" { cmd := e.resetAndSend(value) return e, cmd } @@ -1181,7 +1181,7 @@ func (e *editor) IsRecording() bool { // SendContent triggers sending the current editor content func (e *editor) SendContent() tea.Cmd { value := e.textarea.Value() - if value == "" || e.working { + if value == "" { return nil } return e.resetAndSend(value) diff --git a/pkg/tui/messages/messages.go b/pkg/tui/messages/messages.go index 6f94e0458..3f2e707f5 100644 --- a/pkg/tui/messages/messages.go +++ b/pkg/tui/messages/messages.go @@ -24,6 +24,7 @@ type ( StartSpeakMsg struct{} // Start speech-to-text transcription StopSpeakMsg struct{} // Stop speech-to-text transcription SpeakTranscriptMsg struct{ Delta string } // Transcription delta from speech-to-text + ClearQueueMsg struct{} // Clear all queued messages ) // AgentCommandMsg command message diff --git a/pkg/tui/page/chat/chat.go b/pkg/tui/page/chat/chat.go index 94834424d..e9d4e08c6 100644 --- a/pkg/tui/page/chat/chat.go +++ b/pkg/tui/page/chat/chat.go @@ -20,6 +20,7 @@ import ( "github.com/docker/cagent/pkg/tui/commands" "github.com/docker/cagent/pkg/tui/components/editor" "github.com/docker/cagent/pkg/tui/components/messages" + "github.com/docker/cagent/pkg/tui/components/notification" "github.com/docker/cagent/pkg/tui/components/sidebar" "github.com/docker/cagent/pkg/tui/components/spinner" "github.com/docker/cagent/pkg/tui/core" @@ -68,6 +69,15 @@ type Page interface { SendEditorContent() tea.Cmd } +// queuedMessage represents a message waiting to be sent to the agent +type queuedMessage struct { + content string + attachments map[string]string +} + +// maxQueuedMessages is the maximum number of messages that can be queued +const maxQueuedMessages = 5 + // chatPage implements Page type chatPage struct { width, height int @@ -87,6 +97,9 @@ type chatPage struct { msgCancel context.CancelFunc streamCancelled bool + // Message queue for enqueuing messages while agent is working + messageQueue []queuedMessage + // Key map keyMap KeyMap @@ -315,8 +328,7 @@ func (p *chatPage) Update(msg tea.Msg) (layout.Model, tea.Cmd) { case editor.SendMsg: slog.Debug(msg.Content) - cmd := p.processMessage(msg) - return p, cmd + return p.handleSendMsg(msg) case messages.StreamCancelledMsg: model, cmd := p.messages.Update(msg) @@ -329,6 +341,12 @@ func (p *chatPage) Update(msg tea.Msg) (layout.Model, tea.Cmd) { cmds = append(cmds, p.messages.AddCancelledMessage()) } cmds = append(cmds, p.messages.ScrollToBottom()) + + // Process next queued message after cancel (queue is preserved) + if queueCmd := p.processNextQueuedMessage(); queueCmd != nil { + cmds = append(cmds, queueCmd) + } + return p, tea.Batch(cmds...) case msgtypes.InsertFileRefMsg: @@ -342,6 +360,9 @@ func (p *chatPage) Update(msg tea.Msg) (layout.Model, tea.Cmd) { p.messages = model.(messages.Model) return p, cmd + case msgtypes.ClearQueueMsg: + return p.handleClearQueue() + default: // Try to handle as a runtime event if handled, cmd := p.handleRuntimeEvent(msg); handled { @@ -590,6 +611,74 @@ func (p *chatPage) cancelStream(showCancelMessage bool) tea.Cmd { ) } +// handleSendMsg handles incoming messages from the editor, either processing +// them immediately or queuing them if the agent is busy. +func (p *chatPage) handleSendMsg(msg editor.SendMsg) (layout.Model, tea.Cmd) { + // If not working, process immediately + if !p.working { + cmd := p.processMessage(msg) + return p, cmd + } + + // If queue is full, reject the message + if len(p.messageQueue) >= maxQueuedMessages { + return p, notification.WarningCmd(fmt.Sprintf("Queue full (max %d messages). Please wait.", maxQueuedMessages)) + } + + // Add to queue + p.messageQueue = append(p.messageQueue, queuedMessage{ + content: msg.Content, + attachments: msg.Attachments, + }) + + queueLen := len(p.messageQueue) + var notifyMsg string + if queueLen == 1 { + notifyMsg = "Message queued (1 waiting)" + } else { + notifyMsg = fmt.Sprintf("Message queued (%d waiting)", queueLen) + } + + return p, notification.InfoCmd(notifyMsg) +} + +// processNextQueuedMessage pops the next message from the queue and processes it. +// Returns nil if the queue is empty. +func (p *chatPage) processNextQueuedMessage() tea.Cmd { + if len(p.messageQueue) == 0 { + return nil + } + + // Pop the first message from the queue + queued := p.messageQueue[0] + p.messageQueue = p.messageQueue[1:] + + msg := editor.SendMsg{ + Content: queued.content, + Attachments: queued.attachments, + } + + return p.processMessage(msg) +} + +// handleClearQueue clears all queued messages and shows a notification. +func (p *chatPage) handleClearQueue() (layout.Model, tea.Cmd) { + count := len(p.messageQueue) + if count == 0 { + return p, notification.InfoCmd("No messages queued") + } + + p.messageQueue = nil + + var msg string + if count == 1 { + msg = "Cleared 1 queued message" + } else { + msg = fmt.Sprintf("Cleared %d queued messages", count) + } + return p, notification.SuccessCmd(msg) +} + // processMessage processes a message with the runtime func (p *chatPage) processMessage(msg editor.SendMsg) tea.Cmd { if p.msgCancel != nil { @@ -789,7 +878,20 @@ func (p *chatPage) renderResizeHandle(width int) string { if p.working { // Truncate right side and append spinner (handle stays centered) - suffix := " " + p.spinner.View() + " " + styles.SpinnerDotsHighlightStyle.Render("Working…") + workingText := "Working…" + if queueLen := len(p.messageQueue); queueLen > 0 { + workingText = fmt.Sprintf("Working… (%d queued)", queueLen) + } + suffix := " " + p.spinner.View() + " " + styles.SpinnerDotsHighlightStyle.Render(workingText) + suffixWidth := lipgloss.Width(suffix) + truncated := lipgloss.NewStyle().MaxWidth(width - 2 - suffixWidth).Render(fullLine) + return truncated + suffix + } + + // Show queue count even when not working (messages waiting to be processed) + if queueLen := len(p.messageQueue); queueLen > 0 { + queueText := fmt.Sprintf("%d queued", queueLen) + suffix := " " + styles.WarningStyle.Render(queueText) + " " suffixWidth := lipgloss.Width(suffix) truncated := lipgloss.NewStyle().MaxWidth(width - 2 - suffixWidth).Render(fullLine) return truncated + suffix diff --git a/pkg/tui/page/chat/runtime_events.go b/pkg/tui/page/chat/runtime_events.go index 32d74dc85..b75ac19a3 100644 --- a/pkg/tui/page/chat/runtime_events.go +++ b/pkg/tui/page/chat/runtime_events.go @@ -138,7 +138,11 @@ func (p *chatPage) handleStreamStopped(msg *runtime.StreamStoppedEvent) tea.Cmd p.streamCancelled = false p.stopProgressBar() sidebarCmd := p.forwardToSidebar(msg) - return tea.Batch(p.messages.ScrollToBottom(), spinnerCmd, sidebarCmd) + + // Check if there are queued messages to process + queueCmd := p.processNextQueuedMessage() + + return tea.Batch(p.messages.ScrollToBottom(), spinnerCmd, sidebarCmd, queueCmd) } func (p *chatPage) handlePartialToolCall(msg *runtime.PartialToolCallEvent) tea.Cmd { diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index f9dacdd13..b0377e76a 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -69,6 +69,7 @@ type KeyMap struct { SwitchAgent key.Binding ModelPicker key.Binding Speak key.Binding + ClearQueue key.Binding } // DefaultKeyMap returns the default global key bindings @@ -102,6 +103,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("ctrl+k"), key.WithHelp("Ctrl+k", "speak"), ), + ClearQueue: key.NewBinding( + key.WithKeys("ctrl+x"), + key.WithHelp("Ctrl+x", "clear queue"), + ), } } @@ -297,6 +302,11 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case messages.ToggleHideToolResultsMsg: return a.handleToggleHideToolResults() + case messages.ClearQueueMsg: + updated, cmd := a.chatPage.Update(msg) + a.chatPage = updated.(chat.Page) + return a, cmd + case messages.ShowCostDialogMsg: return a.handleShowCostDialog() @@ -497,6 +507,9 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } return a, notification.InfoCmd("Speech-to-text is only supported on macOS") + case key.Matches(msg, a.keyMap.ClearQueue): + return a, core.CmdHandler(messages.ClearQueueMsg{}) + default: // Handle ctrl+1 through ctrl+9 for quick agent switching if index := parseCtrlNumberKey(msg); index >= 0 { diff --git a/testFile.yaml b/testFile.yaml new file mode 100644 index 000000000..be293ea82 --- /dev/null +++ b/testFile.yaml @@ -0,0 +1,241 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.anthropic.com + body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following message a user sent to an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text, nothing else.\n\nUser message: hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given a conversation history and asked to create a title that captures the main topic.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[],"stream":true}' + url: https://api.anthropic.com/v1/messages + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01HYUcf6xNGPYWDQ1zeuoK31","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Just"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Saying Hello"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":7} } + + event: message_stop + data: {"type":"message_stop" } + + headers: {} + status: 200 OK + code: 200 + duration: 930.585542ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.anthropic.com + body: '{"max_tokens":64000,"messages":[{"content":[{"text":"hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are an AI assistant with expertise in the moby/moby project''s documentation.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{},"type":"object"},"name":"fetch_moby_documentation","description":"Fetch entire documentation file from GitHub repository: moby/moby. Useful for general questions. Always call this tool first if asked about moby/moby."},{"input_schema":{"properties":{"query":{"description":"The search query to find relevant documentation","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_documentation","description":"Semantically search within the fetched documentation from GitHub repository: moby/moby. Useful for specific queries."},{"input_schema":{"properties":{"page":{"description":"Page number to retrieve (starting from 1). Each page contains 30 results.","type":"number"},"query":{"description":"The search query to find relevant code files","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_code","description":"Search for code within the GitHub repository: \"moby/moby\" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant."},{"input_schema":{"properties":{"url":{"description":"The URL of the document or page to fetch","type":"string"}},"required":["url"],"type":"object"},"name":"fetch_generic_url_content","description":"Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation."}],"stream":true}' + url: https://api.anthropic.com/v1/messages + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01TEFSeoZ7xBDfxJNGP6e3jz","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"! I'm an"}} + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" AI assistant with"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" expertise in the moby"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"/moby project's"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation."}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Moby is"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" an"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" open-source project"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" create"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d by Docker to"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" advance"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" containerization an"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d provide"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" container"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" runtime that"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" powers Docker."} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nI"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" can"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" help you with:"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Information"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" about Docker"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" architecture"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and components\n- Docker"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" configuration"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and installation\n- Container"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" management and orchestration"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Networking"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Docker"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Storage"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" options an"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d volume"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" management\n- Security aspects"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" of Docker/"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Moby\n- API"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and usage"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nIs"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" there something"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" specific about the"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" mo"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"by/moby project"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you'd like to learn"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" about"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"?"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" I"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d be happy to assist"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you with your questions"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"."} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":137} } + + event: message_stop + data: {"type":"message_stop" } + + headers: {} + status: 200 OK + code: 200 + duration: 545.253958ms From 50aa7569f7b334b77719cf427bc58ec59d1d143f Mon Sep 17 00:00:00 2001 From: Stanislav Hamara Date: Thu, 15 Jan 2026 15:47:22 +0000 Subject: [PATCH 2/4] Add queued messages to the sidebar --- pkg/tui/components/sidebar/sidebar.go | 35 +++++++++++++++++++++++++++ pkg/tui/page/chat/chat.go | 17 +++++++++++++ 2 files changed, 52 insertions(+) diff --git a/pkg/tui/components/sidebar/sidebar.go b/pkg/tui/components/sidebar/sidebar.go index d6d503177..3174775bf 100644 --- a/pkg/tui/components/sidebar/sidebar.go +++ b/pkg/tui/components/sidebar/sidebar.go @@ -46,6 +46,7 @@ type Model interface { SetAgentSwitching(switching bool) SetToolsetInfo(availableTools int, loading bool) SetSessionStarred(starred bool) + SetQueuedMessages(messages []string) GetSize() (width, height int) LoadFromSession(sess *session.Session) // HandleClick checks if click is on the star and returns true if handled @@ -87,6 +88,7 @@ type model struct { workingAgent string // Name of the agent currently working (empty if none) scrollbar *scrollbar.Model workingDirectory string + queuedMessages []string // Truncated preview of queued messages } // Option is a functional option for configuring the sidebar. @@ -177,6 +179,11 @@ func (m *model) SetSessionStarred(starred bool) { m.sessionStarred = starred } +// SetQueuedMessages sets the list of queued message previews to display +func (m *model) SetQueuedMessages(messages []string) { + m.queuedMessages = messages +} + // HandleClick checks if click is on the star and returns true if it was // x and y are coordinates relative to the sidebar's top-left corner // This does NOT toggle the state - caller should handle that @@ -478,6 +485,7 @@ func (m *model) renderSections(contentWidth int) []string { appendSection(m.sessionInfo(contentWidth)) appendSection(m.tokenUsage(contentWidth)) + appendSection(m.queueSection(contentWidth)) appendSection(m.agentInfo(contentWidth)) appendSection(m.toolsetInfo(contentWidth)) @@ -635,6 +643,33 @@ func (m *model) sessionInfo(contentWidth int) string { return m.renderTab("Session", strings.Join(lines, "\n"), contentWidth) } +// queueSection renders the queued messages section +func (m *model) queueSection(contentWidth int) string { + if len(m.queuedMessages) == 0 { + return "" + } + + maxMsgWidth := contentWidth - treePrefixWidth + var lines []string + + for i, msg := range m.queuedMessages { + // Determine prefix based on position + var prefix string + if i == len(m.queuedMessages)-1 { + prefix = styles.MutedStyle.Render("└ ") + } else { + prefix = styles.MutedStyle.Render("├ ") + } + + // Truncate message and add prefix + truncated := toolcommon.TruncateText(msg, maxMsgWidth) + lines = append(lines, prefix+styles.MutedStyle.Render(truncated)) + } + + title := fmt.Sprintf("Queue (%d)", len(m.queuedMessages)) + return m.renderTab(title, strings.Join(lines, "\n"), contentWidth) +} + // agentInfo renders the current agent information func (m *model) agentInfo(contentWidth int) string { // Read current agent from session state so sidebar updates when agent is switched diff --git a/pkg/tui/page/chat/chat.go b/pkg/tui/page/chat/chat.go index e9d4e08c6..c663f9979 100644 --- a/pkg/tui/page/chat/chat.go +++ b/pkg/tui/page/chat/chat.go @@ -630,6 +630,7 @@ func (p *chatPage) handleSendMsg(msg editor.SendMsg) (layout.Model, tea.Cmd) { content: msg.Content, attachments: msg.Attachments, }) + p.syncQueueToSidebar() queueLen := len(p.messageQueue) var notifyMsg string @@ -652,6 +653,7 @@ func (p *chatPage) processNextQueuedMessage() tea.Cmd { // Pop the first message from the queue queued := p.messageQueue[0] p.messageQueue = p.messageQueue[1:] + p.syncQueueToSidebar() msg := editor.SendMsg{ Content: queued.content, @@ -669,6 +671,7 @@ func (p *chatPage) handleClearQueue() (layout.Model, tea.Cmd) { } p.messageQueue = nil + p.syncQueueToSidebar() var msg string if count == 1 { @@ -679,6 +682,20 @@ func (p *chatPage) handleClearQueue() (layout.Model, tea.Cmd) { return p, notification.SuccessCmd(msg) } +// syncQueueToSidebar updates the sidebar with truncated previews of queued messages. +func (p *chatPage) syncQueueToSidebar() { + previews := make([]string, len(p.messageQueue)) + for i, qm := range p.messageQueue { + // Take first line and limit length for preview + content := strings.TrimSpace(qm.content) + if idx := strings.IndexAny(content, "\n\r"); idx != -1 { + content = content[:idx] + } + previews[i] = content + } + p.sidebar.SetQueuedMessages(previews) +} + // processMessage processes a message with the runtime func (p *chatPage) processMessage(msg editor.SendMsg) tea.Cmd { if p.msgCancel != nil { From 98942a044acde9bebd441dda408c71b5757e7bda Mon Sep 17 00:00:00 2001 From: Stanislav Hamara Date: Thu, 15 Jan 2026 15:50:25 +0000 Subject: [PATCH 3/4] Add hint for the shortcut --- pkg/tui/components/sidebar/sidebar.go | 3 +++ pkg/tui/page/chat/chat.go | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/tui/components/sidebar/sidebar.go b/pkg/tui/components/sidebar/sidebar.go index 3174775bf..ec0b77309 100644 --- a/pkg/tui/components/sidebar/sidebar.go +++ b/pkg/tui/components/sidebar/sidebar.go @@ -666,6 +666,9 @@ func (m *model) queueSection(contentWidth int) string { lines = append(lines, prefix+styles.MutedStyle.Render(truncated)) } + // Add hint for clearing + lines = append(lines, styles.MutedStyle.Render(" Ctrl+X to clear")) + title := fmt.Sprintf("Queue (%d)", len(m.queuedMessages)) return m.renderTab(title, strings.Join(lines, "\n"), contentWidth) } diff --git a/pkg/tui/page/chat/chat.go b/pkg/tui/page/chat/chat.go index c663f9979..83dd384ff 100644 --- a/pkg/tui/page/chat/chat.go +++ b/pkg/tui/page/chat/chat.go @@ -635,9 +635,9 @@ func (p *chatPage) handleSendMsg(msg editor.SendMsg) (layout.Model, tea.Cmd) { queueLen := len(p.messageQueue) var notifyMsg string if queueLen == 1 { - notifyMsg = "Message queued (1 waiting)" + notifyMsg = "Message queued (1 waiting) · Ctrl+X to clear" } else { - notifyMsg = fmt.Sprintf("Message queued (%d waiting)", queueLen) + notifyMsg = fmt.Sprintf("Message queued (%d waiting) · Ctrl+X to clear", queueLen) } return p, notification.InfoCmd(notifyMsg) From 7c503ec44a48c7487984b4ad14478233667feab2 Mon Sep 17 00:00:00 2001 From: Stanislav Hamara Date: Thu, 15 Jan 2026 16:02:50 +0000 Subject: [PATCH 4/4] Some tests Did not mean to commit these Chris comments Lint --- cagent-recording-1767970646.yaml | 256 ----------------------- cagent-recording-1767970795.yaml | 190 ----------------- pkg/tui/components/sidebar/queue_test.go | 99 +++++++++ pkg/tui/components/sidebar/sidebar.go | 2 +- pkg/tui/page/chat/chat.go | 8 +- pkg/tui/page/chat/queue_test.go | 146 +++++++++++++ testFile.yaml | 241 --------------------- 7 files changed, 248 insertions(+), 694 deletions(-) delete mode 100644 cagent-recording-1767970646.yaml delete mode 100644 cagent-recording-1767970795.yaml create mode 100644 pkg/tui/components/sidebar/queue_test.go create mode 100644 pkg/tui/page/chat/queue_test.go delete mode 100644 testFile.yaml diff --git a/cagent-recording-1767970646.yaml b/cagent-recording-1767970646.yaml deleted file mode 100644 index b724105c0..000000000 --- a/cagent-recording-1767970646.yaml +++ /dev/null @@ -1,256 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - host: api.anthropic.com - body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following message a user sent to an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text, nothing else.\n\nUser message: hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given a conversation history and asked to create a title that captures the main topic.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[],"stream":true}' - url: https://api.anthropic.com/v1/messages - method: POST - response: - proto: HTTP/2.0 - proto_major: 2 - proto_minor: 0 - content_length: -1 - body: |+ - event: message_start - data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01P91w2kca4k1cXX3Bi7AGHV","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } - - event: content_block_start - data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Brief"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Greeting"} } - - event: ping - data: {"type": "ping"} - - event: content_block_stop - data: {"type":"content_block_stop","index":0} - - event: ping - data: {"type": "ping"} - - event: ping - data: {"type": "ping"} - - event: message_delta - data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":6} } - - event: message_stop - data: {"type":"message_stop" } - - headers: {} - status: 200 OK - code: 200 - duration: 691.282667ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - host: api.anthropic.com - body: '{"max_tokens":64000,"messages":[{"content":[{"text":"hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are an AI assistant with expertise in the moby/moby project''s documentation.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{},"type":"object"},"name":"fetch_moby_documentation","description":"Fetch entire documentation file from GitHub repository: moby/moby. Useful for general questions. Always call this tool first if asked about moby/moby."},{"input_schema":{"properties":{"query":{"description":"The search query to find relevant documentation","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_documentation","description":"Semantically search within the fetched documentation from GitHub repository: moby/moby. Useful for specific queries."},{"input_schema":{"properties":{"page":{"description":"Page number to retrieve (starting from 1). Each page contains 30 results.","type":"number"},"query":{"description":"The search query to find relevant code files","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_code","description":"Search for code within the GitHub repository: \"moby/moby\" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant."},{"input_schema":{"properties":{"url":{"description":"The URL of the document or page to fetch","type":"string"}},"required":["url"],"type":"object"},"name":"fetch_generic_url_content","description":"Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation."}],"stream":true}' - url: https://api.anthropic.com/v1/messages - method: POST - response: - proto: HTTP/2.0 - proto_major: 2 - proto_minor: 0 - content_length: -1 - body: |+ - event: message_start - data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01Nhz3DnX7RNe6x1a9y5Se1S","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}} - - event: content_block_start - data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"! I'm an AI"} } - - event: ping - data: {"type": "ping"} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" assistant with"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" expertise in the moby"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"/moby project's"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation. The"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" mo"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"by/"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"moby repository"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" is the"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" open"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"-source development"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" repository"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for Docker"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" a platform"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for developing"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":", shipping, and running"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" applications in"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" containers.\n\nIs"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" there something specific about Docker"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" or the moby/"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"moby project that you"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'d like to learn"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" about"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"?"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" I can help you with"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":":"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\n- General information about"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Docker an"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d its"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" components"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Docker"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" installation and configuration"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Docker"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" commands and their"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" usage\n- Docker"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" networking"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" storage, or"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" security"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Docker"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" API"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" an"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d development"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" topics"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- An"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d much more relate"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d to the moby/"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"moby project\n\nFeel"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" free to ask any"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" questions you have,"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and I'll do my"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" best to assist"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you!"} } - - event: content_block_stop - data: {"type":"content_block_stop","index":0} - - event: message_delta - data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":154} } - - event: message_stop - data: {"type":"message_stop" } - - headers: {} - status: 200 OK - code: 200 - duration: 538.213ms diff --git a/cagent-recording-1767970795.yaml b/cagent-recording-1767970795.yaml deleted file mode 100644 index 320f78a67..000000000 --- a/cagent-recording-1767970795.yaml +++ /dev/null @@ -1,190 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - host: api.anthropic.com - body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following message a user sent to an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text, nothing else.\n\nUser message: test","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given a conversation history and asked to create a title that captures the main topic.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[],"stream":true}' - url: https://api.anthropic.com/v1/messages - method: POST - response: - proto: HTTP/2.0 - proto_major: 2 - proto_minor: 0 - content_length: -1 - body: |+ - event: message_start - data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_019HzmEffBbC1QRrN5EQiRcF","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } - - event: content_block_start - data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Test"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Message"} } - - event: ping - data: {"type": "ping"} - - event: content_block_stop - data: {"type":"content_block_stop","index":0 } - - event: ping - data: {"type": "ping"} - - event: ping - data: {"type": "ping"} - - event: message_delta - data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } - - event: message_stop - data: {"type":"message_stop" } - - headers: {} - status: 200 OK - code: 200 - duration: 734.233834ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - host: api.anthropic.com - body: '{"max_tokens":64000,"messages":[{"content":[{"text":"test","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are an AI assistant with expertise in the moby/moby project''s documentation.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{},"type":"object"},"name":"fetch_moby_documentation","description":"Fetch entire documentation file from GitHub repository: moby/moby. Useful for general questions. Always call this tool first if asked about moby/moby."},{"input_schema":{"properties":{"query":{"description":"The search query to find relevant documentation","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_documentation","description":"Semantically search within the fetched documentation from GitHub repository: moby/moby. Useful for specific queries."},{"input_schema":{"properties":{"page":{"description":"Page number to retrieve (starting from 1). Each page contains 30 results.","type":"number"},"query":{"description":"The search query to find relevant code files","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_code","description":"Search for code within the GitHub repository: \"moby/moby\" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant."},{"input_schema":{"properties":{"url":{"description":"The URL of the document or page to fetch","type":"string"}},"required":["url"],"type":"object"},"name":"fetch_generic_url_content","description":"Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation."}],"stream":true}' - url: https://api.anthropic.com/v1/messages - method: POST - response: - proto: HTTP/2.0 - proto_major: 2 - proto_minor: 0 - content_length: -1 - body: |+ - event: message_start - data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01MU1MMAvMthqVk7azXx2eKz","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard"}} } - - event: content_block_start - data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d be happy to help you"} } - - event: ping - data: {"type": "ping"} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with the"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" moby/moby"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" project. Your"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" message"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" \""} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"test\" seems"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" to be a simple"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" test to check"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" if I'm working properly"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":".\n\nTo provide"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with useful information about the"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" moby/moby"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" project ("} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"which is"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Docker Engine"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" core"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"), I can"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" fetch"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" first"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" to"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" give"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you an"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" overview."} } - - event: content_block_stop - data: {"type":"content_block_stop","index":0 } - - event: content_block_start - data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01GLWQA6EprQDqdKGqchE6mE","name":"fetch_moby_documentation","input":{}} } - - event: content_block_delta - data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} } - - event: content_block_stop - data: {"type":"content_block_stop","index":1 } - - event: message_delta - data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":111} } - - event: message_stop - data: {"type":"message_stop" } - - headers: {} - status: 200 OK - code: 200 - duration: 565.07425ms diff --git a/pkg/tui/components/sidebar/queue_test.go b/pkg/tui/components/sidebar/queue_test.go new file mode 100644 index 000000000..929b47f59 --- /dev/null +++ b/pkg/tui/components/sidebar/queue_test.go @@ -0,0 +1,99 @@ +package sidebar + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/docker/cagent/pkg/tui/service" +) + +func TestQueueSection_SingleMessage(t *testing.T) { + t.Parallel() + + sessionState := &service.SessionState{} + m := New(sessionState).(*model) + + m.SetQueuedMessages([]string{"Hello world"}) + + result := m.queueSection(40) + + // Should contain the title with count + assert.Contains(t, result, "Queue (1)") + + // Should contain the message + assert.Contains(t, result, "Hello world") + + // Should contain the clear hint + assert.Contains(t, result, "Ctrl+X to clear") + + // Should use └ prefix for single (last) item + assert.Contains(t, result, "└") +} + +func TestQueueSection_MultipleMessages(t *testing.T) { + t.Parallel() + + sessionState := &service.SessionState{} + m := New(sessionState).(*model) + + m.SetQueuedMessages([]string{"First", "Second", "Third"}) + + result := m.queueSection(40) + + // Should contain the title with count + assert.Contains(t, result, "Queue (3)") + + // Should contain all messages + assert.Contains(t, result, "First") + assert.Contains(t, result, "Second") + assert.Contains(t, result, "Third") + + // Should contain the clear hint + assert.Contains(t, result, "Ctrl+X to clear") + + // Should have tree-style prefixes + assert.Contains(t, result, "├") // For non-last items + assert.Contains(t, result, "└") // For last item +} + +func TestQueueSection_LongMessageTruncation(t *testing.T) { + t.Parallel() + + sessionState := &service.SessionState{} + m := New(sessionState).(*model) + + // Create a very long message + longMessage := strings.Repeat("x", 100) + m.SetQueuedMessages([]string{longMessage}) + + result := m.queueSection(30) // Narrow width to force truncation + + // Should contain truncation indicator + require.NotEmpty(t, result) + + // The full long message should not appear (it's truncated) + assert.NotContains(t, result, longMessage) +} + +func TestQueueSection_InRenderSections(t *testing.T) { + t.Parallel() + + sessionState := &service.SessionState{} + m := New(sessionState).(*model) + m.SetSize(40, 100) // Set a reasonable size + + // Without queued messages, queue section should not appear in output + linesWithoutQueue := m.renderSections(35) + outputWithoutQueue := strings.Join(linesWithoutQueue, "\n") + assert.NotContains(t, outputWithoutQueue, "Queue") + + // With queued messages, queue section should appear + m.SetQueuedMessages([]string{"Pending task"}) + linesWithQueue := m.renderSections(35) + outputWithQueue := strings.Join(linesWithQueue, "\n") + assert.Contains(t, outputWithQueue, "Queue (1)") + assert.Contains(t, outputWithQueue, "Pending task") +} diff --git a/pkg/tui/components/sidebar/sidebar.go b/pkg/tui/components/sidebar/sidebar.go index ec0b77309..a2ddff261 100644 --- a/pkg/tui/components/sidebar/sidebar.go +++ b/pkg/tui/components/sidebar/sidebar.go @@ -663,7 +663,7 @@ func (m *model) queueSection(contentWidth int) string { // Truncate message and add prefix truncated := toolcommon.TruncateText(msg, maxMsgWidth) - lines = append(lines, prefix+styles.MutedStyle.Render(truncated)) + lines = append(lines, prefix+truncated) } // Add hint for clearing diff --git a/pkg/tui/page/chat/chat.go b/pkg/tui/page/chat/chat.go index 83dd384ff..fe5c0a5af 100644 --- a/pkg/tui/page/chat/chat.go +++ b/pkg/tui/page/chat/chat.go @@ -633,12 +633,7 @@ func (p *chatPage) handleSendMsg(msg editor.SendMsg) (layout.Model, tea.Cmd) { p.syncQueueToSidebar() queueLen := len(p.messageQueue) - var notifyMsg string - if queueLen == 1 { - notifyMsg = "Message queued (1 waiting) · Ctrl+X to clear" - } else { - notifyMsg = fmt.Sprintf("Message queued (%d waiting) · Ctrl+X to clear", queueLen) - } + notifyMsg := fmt.Sprintf("Message queued (%d waiting) · Ctrl+X to clear", queueLen) return p, notification.InfoCmd(notifyMsg) } @@ -652,6 +647,7 @@ func (p *chatPage) processNextQueuedMessage() tea.Cmd { // Pop the first message from the queue queued := p.messageQueue[0] + p.messageQueue[0] = queuedMessage{} // zero out to allow GC p.messageQueue = p.messageQueue[1:] p.syncQueueToSidebar() diff --git a/pkg/tui/page/chat/queue_test.go b/pkg/tui/page/chat/queue_test.go new file mode 100644 index 000000000..db3e9ae17 --- /dev/null +++ b/pkg/tui/page/chat/queue_test.go @@ -0,0 +1,146 @@ +package chat + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/docker/cagent/pkg/tui/components/editor" + "github.com/docker/cagent/pkg/tui/components/sidebar" + "github.com/docker/cagent/pkg/tui/service" +) + +// newTestChatPage creates a minimal chatPage for testing queue behavior. +// Note: This only initializes fields needed for queue testing. +// processMessage cannot be called without full initialization. +func newTestChatPage(t *testing.T) *chatPage { + t.Helper() + sessionState := &service.SessionState{} + + return &chatPage{ + sidebar: sidebar.New(sessionState), + sessionState: sessionState, + working: true, // Start busy so messages get queued + } +} + +func TestQueueFlow_BusyAgent_QueuesMessage(t *testing.T) { + t.Parallel() + + p := newTestChatPage(t) + // newTestChatPage already sets working=true + + // Send first message while busy + msg1 := editor.SendMsg{Content: "first message"} + _, cmd := p.handleSendMsg(msg1) + + // Should be queued + require.Len(t, p.messageQueue, 1) + assert.Equal(t, "first message", p.messageQueue[0].content) + // Command should be a notification (not processMessage) + assert.NotNil(t, cmd) + + // Send second message while still busy + msg2 := editor.SendMsg{Content: "second message"} + _, _ = p.handleSendMsg(msg2) + + require.Len(t, p.messageQueue, 2) + assert.Equal(t, "first message", p.messageQueue[0].content) + assert.Equal(t, "second message", p.messageQueue[1].content) + + // Send third message + msg3 := editor.SendMsg{Content: "third message"} + _, _ = p.handleSendMsg(msg3) + + require.Len(t, p.messageQueue, 3) +} + +func TestQueueFlow_QueueFull_RejectsMessage(t *testing.T) { + t.Parallel() + + p := newTestChatPage(t) + // newTestChatPage sets working=true + + // Fill the queue to max + for i := range maxQueuedMessages { + msg := editor.SendMsg{Content: "message"} + _, _ = p.handleSendMsg(msg) + assert.Len(t, p.messageQueue, i+1) + } + + require.Len(t, p.messageQueue, maxQueuedMessages) + + // Try to add one more - should be rejected + msg := editor.SendMsg{Content: "overflow message"} + _, cmd := p.handleSendMsg(msg) + + // Queue size should not change + assert.Len(t, p.messageQueue, maxQueuedMessages) + // Should return a warning notification command + assert.NotNil(t, cmd) +} + +func TestQueueFlow_PopFromQueue(t *testing.T) { + t.Parallel() + + p := newTestChatPage(t) + + // Queue some messages + p.handleSendMsg(editor.SendMsg{Content: "first"}) + p.handleSendMsg(editor.SendMsg{Content: "second"}) + p.handleSendMsg(editor.SendMsg{Content: "third"}) + + require.Len(t, p.messageQueue, 3) + + // Manually pop messages (simulating what processNextQueuedMessage does internally) + // Pop first + popped := p.messageQueue[0] + p.messageQueue = p.messageQueue[1:] + p.syncQueueToSidebar() + + assert.Equal(t, "first", popped.content) + require.Len(t, p.messageQueue, 2) + assert.Equal(t, "second", p.messageQueue[0].content) + assert.Equal(t, "third", p.messageQueue[1].content) + + // Pop second + popped = p.messageQueue[0] + p.messageQueue = p.messageQueue[1:] + + assert.Equal(t, "second", popped.content) + require.Len(t, p.messageQueue, 1) + assert.Equal(t, "third", p.messageQueue[0].content) + + // Pop last + popped = p.messageQueue[0] + p.messageQueue = p.messageQueue[1:] + + assert.Equal(t, "third", popped.content) + assert.Empty(t, p.messageQueue) +} + +func TestQueueFlow_ClearQueue(t *testing.T) { + t.Parallel() + + p := newTestChatPage(t) + // newTestChatPage sets working=true + + // Queue some messages + p.handleSendMsg(editor.SendMsg{Content: "first"}) + p.handleSendMsg(editor.SendMsg{Content: "second"}) + p.handleSendMsg(editor.SendMsg{Content: "third"}) + + require.Len(t, p.messageQueue, 3) + + // Clear the queue + _, cmd := p.handleClearQueue() + + assert.Empty(t, p.messageQueue) + assert.NotNil(t, cmd) // Success notification + + // Clearing empty queue + _, cmd = p.handleClearQueue() + assert.Empty(t, p.messageQueue) + assert.NotNil(t, cmd) // Info notification +} diff --git a/testFile.yaml b/testFile.yaml deleted file mode 100644 index be293ea82..000000000 --- a/testFile.yaml +++ /dev/null @@ -1,241 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - host: api.anthropic.com - body: '{"max_tokens":20,"messages":[{"content":[{"text":"Based on the following message a user sent to an AI assistant, generate a short, descriptive title (maximum 50 characters) that captures the main topic or purpose of the conversation. Return ONLY the title text, nothing else.\n\nUser message: hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are a helpful AI assistant that generates concise, descriptive titles for conversations. You will be given a conversation history and asked to create a title that captures the main topic.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[],"stream":true}' - url: https://api.anthropic.com/v1/messages - method: POST - response: - proto: HTTP/2.0 - proto_major: 2 - proto_minor: 0 - content_length: -1 - body: |+ - event: message_start - data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01HYUcf6xNGPYWDQ1zeuoK31","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } - - event: content_block_start - data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Just"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Saying Hello"} } - - event: content_block_stop - data: {"type":"content_block_stop","index":0 } - - event: message_delta - data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":96,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":7} } - - event: message_stop - data: {"type":"message_stop" } - - headers: {} - status: 200 OK - code: 200 - duration: 930.585542ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - host: api.anthropic.com - body: '{"max_tokens":64000,"messages":[{"content":[{"text":"hi","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-3-7-sonnet-20250219","system":[{"text":"You are an AI assistant with expertise in the moby/moby project''s documentation.","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{},"type":"object"},"name":"fetch_moby_documentation","description":"Fetch entire documentation file from GitHub repository: moby/moby. Useful for general questions. Always call this tool first if asked about moby/moby."},{"input_schema":{"properties":{"query":{"description":"The search query to find relevant documentation","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_documentation","description":"Semantically search within the fetched documentation from GitHub repository: moby/moby. Useful for specific queries."},{"input_schema":{"properties":{"page":{"description":"Page number to retrieve (starting from 1). Each page contains 30 results.","type":"number"},"query":{"description":"The search query to find relevant code files","type":"string"}},"required":["query"],"type":"object"},"name":"search_moby_code","description":"Search for code within the GitHub repository: \"moby/moby\" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant."},{"input_schema":{"properties":{"url":{"description":"The URL of the document or page to fetch","type":"string"}},"required":["url"],"type":"object"},"name":"fetch_generic_url_content","description":"Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation."}],"stream":true}' - url: https://api.anthropic.com/v1/messages - method: POST - response: - proto: HTTP/2.0 - proto_major: 2 - proto_minor: 0 - content_length: -1 - body: |+ - event: message_start - data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_01TEFSeoZ7xBDfxJNGP6e3jz","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } - - event: content_block_start - data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"! I'm an"}} - - event: ping - data: {"type": "ping"} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" AI assistant with"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" expertise in the moby"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"/moby project's"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation."}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Moby is"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" an"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" open-source project"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" create"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d by Docker to"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" advance"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" containerization an"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d provide"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" container"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" runtime that"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" powers Docker."} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nI"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" can"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" help you with:"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Information"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" about Docker"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" architecture"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and components\n- Docker"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" configuration"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and installation\n- Container"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" management and orchestration"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Networking"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Docker"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- Storage"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" options an"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d volume"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" management\n- Security aspects"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" of Docker/"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Moby\n- API"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" documentation"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and usage"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nIs"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" there something"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" specific about the"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" mo"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"by/moby project"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you'd like to learn"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" about"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"?"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" I"}} - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d be happy to assist"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you with your questions"} } - - event: content_block_delta - data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"."} } - - event: content_block_stop - data: {"type":"content_block_stop","index":0 } - - event: message_delta - data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":729,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":137} } - - event: message_stop - data: {"type":"message_stop" } - - headers: {} - status: 200 OK - code: 200 - duration: 545.253958ms