From 5359026b89490499deebd09adc4f605a3de264dd Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Sun, 14 Sep 2025 23:18:12 +0530 Subject: [PATCH 1/9] feat: integrate SDK --- cmd/attach/attach.go | 40 ++-- cmd/server/server.go | 49 ++-- lib/{httpapi => cli}/claude.go | 6 +- lib/cli/cli_handler.go | 205 ++++++++++++++++ lib/{httpapi => cli}/events.go | 63 ++--- lib/cli/events_test.go | 102 ++++++++ lib/{ => cli}/msgfmt/message_box.go | 0 lib/{ => cli}/msgfmt/msgfmt.go | 22 +- lib/{ => cli}/msgfmt/msgfmt_test.go | 0 .../format/aider/first_message/expected.txt | 0 .../format/aider/first_message/msg.txt | 0 .../format/aider/first_message/user.txt | 0 .../aider/multi-line-input/expected.txt | 0 .../format/aider/multi-line-input/msg.txt | 0 .../format/aider/multi-line-input/user.txt | 0 .../format/aider/second_message/expected.txt | 0 .../format/aider/second_message/msg.txt | 0 .../format/aider/second_message/user.txt | 0 .../amazonq/confirmation_box/expected.txt | 0 .../format/amazonq/confirmation_box/msg.txt | 0 .../format/amazonq/confirmation_box/user.txt | 0 .../format/amazonq/first_message/expected.txt | 0 .../format/amazonq/first_message/msg.txt | 0 .../format/amazonq/first_message/user.txt | 0 .../amazonq/multi-line-input/expected.txt | 0 .../format/amazonq/multi-line-input/msg.txt | 0 .../format/amazonq/multi-line-input/user.txt | 0 .../amazonq/second_message/expected.txt | 0 .../format/amazonq/second_message/msg.txt | 0 .../format/amazonq/second_message/user.txt | 0 .../format/amazonq/thinking/expected.txt | 0 .../testdata/format/amazonq/thinking/msg.txt | 0 .../testdata/format/amazonq/thinking/user.txt | 0 .../format/amp/first_message/expected.txt | 0 .../testdata/format/amp/first_message/msg.txt | 0 .../format/amp/first_message/user.txt | 0 .../format/amp/multi-line-input/expected.txt | 0 .../format/amp/multi-line-input/msg.txt | 0 .../format/amp/multi-line-input/user.txt | 0 .../format/amp/second_message/expected.txt | 0 .../format/amp/second_message/msg.txt | 0 .../format/amp/second_message/user.txt | 0 .../format/auggie/first_message/expected.txt | 0 .../format/auggie/first_message/msg.txt | 0 .../format/auggie/first_message/user.txt | 0 .../auggie/multi-line-input/expected.txt | 0 .../format/auggie/multi-line-input/msg.txt | 0 .../format/auggie/multi-line-input/user.txt | 0 .../format/auggie/second_message/expected.txt | 0 .../format/auggie/second_message/msg.txt | 0 .../format/auggie/second_message/user.txt | 0 .../format/auggie/thinking/expected.txt | 0 .../testdata/format/auggie/thinking/msg.txt | 0 .../testdata/format/auggie/thinking/user.txt | 0 .../claude/auto-accept-edits/expected.txt | 0 .../format/claude/auto-accept-edits/msg.txt | 0 .../format/claude/auto-accept-edits/user.txt | 0 .../format/claude/first_message/expected.txt | 0 .../format/claude/first_message/msg.txt | 0 .../format/claude/first_message/user.txt | 0 .../claude/multi-line-input/expected.txt | 0 .../format/claude/multi-line-input/msg.txt | 0 .../format/claude/multi-line-input/user.txt | 0 .../format/claude/second_message/expected.txt | 0 .../format/claude/second_message/msg.txt | 0 .../format/claude/second_message/user.txt | 0 .../codex/confirmation_box/expected.txt | 0 .../format/codex/confirmation_box/msg.txt | 0 .../format/codex/confirmation_box/user.txt | 0 .../format/codex/first_message/expected.txt | 0 .../format/codex/first_message/msg.txt | 0 .../format/codex/first_message/user.txt | 0 .../codex/multi-line-input/expected.txt | 0 .../format/codex/multi-line-input/msg.txt | 0 .../format/codex/multi-line-input/user.txt | 0 .../format/codex/second_message/expected.txt | 0 .../format/codex/second_message/msg.txt | 0 .../format/codex/second_message/user.txt | 0 .../format/codex/thinking/expected.txt | 0 .../testdata/format/codex/thinking/msg.txt | 0 .../testdata/format/codex/thinking/user.txt | 0 .../cursor/confirmation_box/expected.txt | 0 .../format/cursor/confirmation_box/msg.txt | 0 .../format/cursor/confirmation_box/user.txt | 0 .../format/cursor/first_message/expected.txt | 0 .../format/cursor/first_message/msg.txt | 0 .../format/cursor/first_message/user.txt | 0 .../cursor/multi-line-input/expected.txt | 0 .../format/cursor/multi-line-input/msg.txt | 0 .../format/cursor/multi-line-input/user.txt | 0 .../format/cursor/second_message/expected.txt | 0 .../format/cursor/second_message/msg.txt | 0 .../format/cursor/second_message/user.txt | 0 .../format/cursor/thinking/expected.txt | 0 .../testdata/format/cursor/thinking/msg.txt | 0 .../testdata/format/cursor/thinking/user.txt | 0 .../format/gemini/first_message/expected.txt | 0 .../format/gemini/first_message/msg.txt | 0 .../format/gemini/first_message/user.txt | 0 .../gemini/multi-line-input/expected.txt | 0 .../format/gemini/multi-line-input/msg.txt | 0 .../format/gemini/multi-line-input/user.txt | 0 .../format/gemini/second_message/expected.txt | 0 .../format/gemini/second_message/msg.txt | 0 .../format/gemini/second_message/user.txt | 0 .../format/goose/first_message/expected.txt | 0 .../format/goose/first_message/msg.txt | 0 .../format/goose/first_message/user.txt | 0 .../goose/multi-line-input/expected.txt | 0 .../format/goose/multi-line-input/msg.txt | 0 .../format/goose/multi-line-input/user.txt | 0 .../format/goose/second_message/expected.txt | 0 .../format/goose/second_message/msg.txt | 0 .../format/goose/second_message/user.txt | 0 .../opencode/first_message/expected.txt | 0 .../format/opencode/first_message/msg.txt | 0 .../format/opencode/first_message/user.txt | 0 .../opencode/second_message/expected.txt | 0 .../format/opencode/second_message/msg.txt | 0 .../format/opencode/second_message/user.txt | 0 .../format/opencode/thinking/expected.txt | 0 .../testdata/format/opencode/thinking/msg.txt | 0 .../format/opencode/thinking/user.txt | 0 .../remove-user-input/aider/expected.txt | 0 .../testdata/remove-user-input/aider/msg.txt | 0 .../testdata/remove-user-input/aider/user.txt | 0 .../remove-user-input/claude/expected.txt | 0 .../testdata/remove-user-input/claude/msg.txt | 0 .../remove-user-input/claude/user.txt | 0 .../empty-input/expected.txt | 0 .../remove-user-input/empty-input/msg.txt | 0 .../remove-user-input/empty-input/user.txt | 0 .../remove-user-input/full-match/expected.txt | 0 .../remove-user-input/full-match/msg.txt | 0 .../remove-user-input/full-match/user.txt | 0 .../remove-user-input/goose/expected.txt | 0 .../testdata/remove-user-input/goose/msg.txt | 0 .../testdata/remove-user-input/goose/user.txt | 0 .../no-user-input-in-message/expected.txt | 0 .../no-user-input-in-message/msg.txt | 0 .../no-user-input-in-message/user.txt | 0 .../non-ascii-2/expected.txt | 0 .../remove-user-input/non-ascii-2/msg.txt | 0 .../remove-user-input/non-ascii-2/user.txt | 0 .../non-ascii-3/expected.txt | 0 .../remove-user-input/non-ascii-3/msg.txt | 0 .../remove-user-input/non-ascii-3/user.txt | 0 .../remove-user-input/non-ascii/expected.txt | 0 .../remove-user-input/non-ascii/msg.txt | 0 .../remove-user-input/non-ascii/user.txt | 0 lib/{ => cli}/screentracker/conversation.go | 42 +--- .../screentracker/conversation_test.go | 5 +- lib/{ => cli}/screentracker/ringbuffer.go | 0 .../testdata/diff/basic/after.txt | 0 .../testdata/diff/basic/before.txt | 0 .../testdata/diff/basic/expected.txt | 0 .../testdata/diff/no-change/after.txt | 0 .../testdata/diff/no-change/before.txt | 0 .../testdata/diff/no-change/expected.txt | 0 lib/{ => cli}/termexec/termexec.go | 0 lib/httpapi/events_test.go | 100 -------- lib/httpapi/handler.go | 18 ++ lib/httpapi/server.go | 219 +++--------------- lib/httpapi/server_test.go | 2 +- lib/httpapi/setup.go | 4 +- lib/sdk/agents/claude/claude.go | 1 + lib/sdk/conversation.go | 9 + lib/sdk/sdk_handler.go | 66 ++++++ lib/types/conversation.go | 70 ++++++ lib/{httpapi => types}/models.go | 49 ++-- 170 files changed, 627 insertions(+), 445 deletions(-) rename lib/{httpapi => cli}/claude.go (90%) create mode 100644 lib/cli/cli_handler.go rename lib/{httpapi => cli}/events.go (71%) create mode 100644 lib/cli/events_test.go rename lib/{ => cli}/msgfmt/message_box.go (100%) rename lib/{ => cli}/msgfmt/msgfmt.go (95%) rename lib/{ => cli}/msgfmt/msgfmt_test.go (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/aider/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/confirmation_box/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/confirmation_box/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/confirmation_box/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/thinking/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/thinking/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amazonq/thinking/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/amp/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/thinking/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/thinking/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/auggie/thinking/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/auto-accept-edits/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/auto-accept-edits/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/auto-accept-edits/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/claude/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/confirmation_box/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/confirmation_box/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/confirmation_box/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/thinking/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/thinking/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/codex/thinking/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/confirmation_box/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/confirmation_box/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/confirmation_box/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/thinking/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/thinking/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/cursor/thinking/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/gemini/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/goose/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/second_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/second_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/second_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/thinking/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/thinking/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/opencode/thinking/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/aider/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/aider/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/aider/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/claude/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/claude/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/claude/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/empty-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/empty-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/empty-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/full-match/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/full-match/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/full-match/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/goose/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/goose/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/goose/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/no-user-input-in-message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/no-user-input-in-message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/no-user-input-in-message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii-2/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii-2/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii-2/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii-3/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii-3/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii-3/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/remove-user-input/non-ascii/user.txt (100%) rename lib/{ => cli}/screentracker/conversation.go (92%) rename lib/{ => cli}/screentracker/conversation_test.go (99%) rename lib/{ => cli}/screentracker/ringbuffer.go (100%) rename lib/{ => cli}/screentracker/testdata/diff/basic/after.txt (100%) rename lib/{ => cli}/screentracker/testdata/diff/basic/before.txt (100%) rename lib/{ => cli}/screentracker/testdata/diff/basic/expected.txt (100%) rename lib/{ => cli}/screentracker/testdata/diff/no-change/after.txt (100%) rename lib/{ => cli}/screentracker/testdata/diff/no-change/before.txt (100%) rename lib/{ => cli}/screentracker/testdata/diff/no-change/expected.txt (100%) rename lib/{ => cli}/termexec/termexec.go (100%) delete mode 100644 lib/httpapi/events_test.go create mode 100644 lib/httpapi/handler.go create mode 100644 lib/sdk/agents/claude/claude.go create mode 100644 lib/sdk/conversation.go create mode 100644 lib/sdk/sdk_handler.go create mode 100644 lib/types/conversation.go rename lib/{httpapi => types}/models.go (56%) diff --git a/cmd/attach/attach.go b/cmd/attach/attach.go index 8bbb409..522f2f4 100644 --- a/cmd/attach/attach.go +++ b/cmd/attach/attach.go @@ -13,7 +13,7 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" - "github.com/coder/agentapi/lib/httpapi" + "github.com/coder/agentapi/lib/types" "github.com/spf13/cobra" sse "github.com/tmaxmax/go-sse" "golang.org/x/term" @@ -35,7 +35,7 @@ func (c *ChannelWriter) Receive() ([]byte, bool) { } type model struct { - screen string + conversation string } func (m model) Init() tea.Cmd { @@ -43,8 +43,8 @@ func (m model) Init() tea.Cmd { return nil } -type screenMsg struct { - screen string +type conversationMsg struct { + conversation string } type finishMsg struct{} @@ -52,10 +52,10 @@ type finishMsg struct{} //lint:ignore U1000 The Update function is used by the Bubble Tea framework func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case screenMsg: - m.screen = msg.screen - if m.screen != "" && m.screen[len(m.screen)-1] != '\n' { - m.screen += "\n" + case conversationMsg: + m.conversation = msg.conversation + if m.conversation != "" && m.conversation[len(m.conversation)-1] != '\n' { + m.conversation += "\n" } case tea.KeyMsg: if msg.String() == "ctrl+c" { @@ -69,10 +69,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m model) View() string { - return m.screen + return m.conversation } -func ReadScreenOverHTTP(ctx context.Context, url string, ch chan<- httpapi.ScreenUpdateBody) error { +func ReadScreenOverHTTP(ctx context.Context, url string, ch chan<- types.ScreenUpdateBody) error { req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req.Header.Set("Content-Type", "application/json") @@ -85,16 +85,16 @@ func ReadScreenOverHTTP(ctx context.Context, url string, ch chan<- httpapi.Scree }() for ev, err := range sse.Read(res.Body, &sse.ReadConfig{ - // 256KB: screen can be big. The default terminal size is 80x1000, + // 256KB: conversation can be big. The default terminal size is 80x1000, // which can be over 80000 bytes. MaxEventSize: 256 * 1024, }) { if err != nil { return xerrors.Errorf("failed to read sse: %w", err) } - var screen httpapi.ScreenUpdateBody + var screen types.ScreenUpdateBody if err := json.Unmarshal([]byte(ev.Data), &screen); err != nil { - return xerrors.Errorf("failed to unmarshal screen: %w", err) + return xerrors.Errorf("failed to unmarshal conversation: %w", err) } ch <- screen } @@ -102,8 +102,8 @@ func ReadScreenOverHTTP(ctx context.Context, url string, ch chan<- httpapi.Scree } func WriteRawInputOverHTTP(ctx context.Context, url string, msg string) error { - messageRequest := httpapi.MessageRequestBody{ - Type: httpapi.MessageTypeRaw, + messageRequest := types.MessageRequestBody{ + Type: types.MessageTypeRaw, Content: msg, } messageRequestBytes, err := json.Marshal(messageRequest) @@ -145,16 +145,16 @@ func runAttach(remoteUrl string) error { } tee := io.TeeReader(os.Stdin, stdinWriter) p := tea.NewProgram(model{}, tea.WithInput(tee), tea.WithAltScreen()) - screenCh := make(chan httpapi.ScreenUpdateBody, 64) + screenCh := make(chan types.ScreenUpdateBody, 64) readScreenErrCh := make(chan error, 1) go func() { defer close(readScreenErrCh) - if err := ReadScreenOverHTTP(ctx, remoteUrl+"/internal/screen", screenCh); err != nil { + if err := ReadScreenOverHTTP(ctx, remoteUrl+"/internal/conversation", screenCh); err != nil { if errors.Is(err, context.Canceled) { return } - readScreenErrCh <- xerrors.Errorf("failed to read screen: %w", err) + readScreenErrCh <- xerrors.Errorf("failed to read conversation: %w", err) } }() writeRawInputErrCh := make(chan error, 1) @@ -189,8 +189,8 @@ func runAttach(remoteUrl string) error { if !ok { return } - p.Send(screenMsg{ - screen: screenUpdate.Screen, + p.Send(conversationMsg{ + conversation: screenUpdate.Screen, }) } } diff --git a/cmd/server/server.go b/cmd/server/server.go index 6581e37..c951112 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -10,14 +10,15 @@ import ( "sort" "strings" + "github.com/coder/agentapi/lib/cli/msgfmt" + "github.com/coder/agentapi/lib/cli/termexec" + "github.com/coder/agentapi/lib/types" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/xerrors" "github.com/coder/agentapi/lib/httpapi" "github.com/coder/agentapi/lib/logctx" - "github.com/coder/agentapi/lib/msgfmt" - "github.com/coder/agentapi/lib/termexec" ) type AgentType = msgfmt.AgentType @@ -68,14 +69,25 @@ func parseAgentType(firstArg string, agentTypeVar string) (AgentType, error) { return AgentTypeCustom, nil } +func parseInteractionType(interactionModeVar string) (types.InteractionType, error) { + return types.CLIInteractionType, nil +} + func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) error { agent := argsToPass[0] agentTypeValue := viper.GetString(FlagType) + interactionTypeValue := viper.GetString(FlagInteractionType) + agentType, err := parseAgentType(agent, agentTypeValue) if err != nil { return xerrors.Errorf("failed to parse agent type: %w", err) } + interactionType, err := parseInteractionType(interactionTypeValue) + if err != nil { + return xerrors.Errorf("failed to parse interaction type: %w", err) + } + termWidth := viper.GetUint16(FlagTermWidth) termHeight := viper.GetUint16(FlagTermHeight) @@ -104,12 +116,13 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er } port := viper.GetInt(FlagPort) srv, err := httpapi.NewServer(ctx, httpapi.ServerConfig{ - AgentType: agentType, - Process: process, - Port: port, - ChatBasePath: viper.GetString(FlagChatBasePath), - AllowedHosts: viper.GetStringSlice(FlagAllowedHosts), - AllowedOrigins: viper.GetStringSlice(FlagAllowedOrigins), + AgentType: agentType, + Process: process, + Port: port, + InteractionType: interactionType, + ChatBasePath: viper.GetString(FlagChatBasePath), + AllowedHosts: viper.GetStringSlice(FlagAllowedHosts), + AllowedOrigins: viper.GetStringSlice(FlagAllowedOrigins), }) if err != nil { return xerrors.Errorf("failed to create server: %w", err) @@ -118,7 +131,6 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er fmt.Println(srv.GetOpenAPI()) return nil } - srv.StartSnapshotLoop(ctx) logger.Info("Starting server on port", "port", port) processExitCh := make(chan error, 1) go func() { @@ -163,15 +175,16 @@ type flagSpec struct { } const ( - FlagType = "type" - FlagPort = "port" - FlagPrintOpenAPI = "print-openapi" - FlagChatBasePath = "chat-base-path" - FlagTermWidth = "term-width" - FlagTermHeight = "term-height" - FlagAllowedHosts = "allowed-hosts" - FlagAllowedOrigins = "allowed-origins" - FlagExit = "exit" + FlagType = "type" + FlagPort = "port" + FlagPrintOpenAPI = "print-openapi" + FlagChatBasePath = "chat-base-path" + FlagTermWidth = "term-width" + FlagTermHeight = "term-height" + FlagAllowedHosts = "allowed-hosts" + FlagAllowedOrigins = "allowed-origins" + FlagExit = "exit" + FlagInteractionType = "interaction" ) func CreateServerCmd() *cobra.Command { diff --git a/lib/httpapi/claude.go b/lib/cli/claude.go similarity index 90% rename from lib/httpapi/claude.go rename to lib/cli/claude.go index 641efe4..58ab001 100644 --- a/lib/httpapi/claude.go +++ b/lib/cli/claude.go @@ -1,8 +1,8 @@ -package httpapi +package cli import ( - mf "github.com/coder/agentapi/lib/msgfmt" - st "github.com/coder/agentapi/lib/screentracker" + mf "github.com/coder/agentapi/lib/cli/msgfmt" + st "github.com/coder/agentapi/lib/cli/screentracker" ) func formatPaste(message string) []st.MessagePart { diff --git a/lib/cli/cli_handler.go b/lib/cli/cli_handler.go new file mode 100644 index 0000000..f500aa8 --- /dev/null +++ b/lib/cli/cli_handler.go @@ -0,0 +1,205 @@ +package cli + +import ( + "context" + "fmt" + "log/slog" + "sync" + "time" + + mf "github.com/coder/agentapi/lib/cli/msgfmt" + st "github.com/coder/agentapi/lib/cli/screentracker" + "github.com/coder/agentapi/lib/cli/termexec" + "github.com/coder/agentapi/lib/types" + "github.com/danielgtaylor/huma/v2/sse" + "golang.org/x/xerrors" +) + +type CLIHandler struct { + emitter *EventEmitter + conversation *st.Conversation + agentio *termexec.Process + mu sync.RWMutex + agentType mf.AgentType + logger *slog.Logger +} + +const snapshotInterval = 25 * time.Millisecond + +func convertStatus(status st.ConversationStatus) types.AgentStatus { + switch status { + case st.ConversationStatusInitializing: + return types.AgentStatusRunning + case st.ConversationStatusStable: + return types.AgentStatusStable + case st.ConversationStatusChanging: + return types.AgentStatusRunning + default: + panic(fmt.Sprintf("unknown conversation status: %s", status)) + } +} + +func NewCLIHandler(ctx context.Context, agentio *termexec.Process, agentType mf.AgentType) *CLIHandler { + formatMessage := func(message string, userInput string) string { + return mf.FormatAgentMessage(agentType, message, userInput) + } + + conversation := st.NewConversation(ctx, st.ConversationConfig{ + AgentIO: agentio, + GetTime: func() time.Time { + return time.Now() + }, + SnapshotInterval: 25 * time.Millisecond, + ScreenStabilityLength: 2 * time.Second, + FormatMessage: formatMessage, + }) + + emitter := NewEventEmitter(1024) + + handler := &CLIHandler{ + emitter: emitter, + conversation: conversation, + agentio: agentio, + agentType: agentType, + } + + handler.StartSnapshotLoop(ctx) + + return handler +} + +// GetStatus handles GET /status +func (c *CLIHandler) GetStatus(ctx context.Context, input *struct{}) (*types.StatusResponse, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + status := c.conversation.Status() + agentStatus := convertStatus(status) + + resp := &types.StatusResponse{} + resp.Body.Status = agentStatus + + return resp, nil +} + +// GetMessages handles GET /messages +func (c *CLIHandler) GetMessages(ctx context.Context, input *struct{}) (*types.MessagesResponse, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + resp := &types.MessagesResponse{} + resp.Body.Messages = make([]types.Message, len(c.conversation.Messages())) + for i, msg := range c.conversation.Messages() { + resp.Body.Messages[i] = types.Message{ + Id: msg.Id, + Role: msg.Role, + Content: msg.Message, + Time: msg.Time, + } + } + + return resp, nil +} + +// CreateMessage handles POST /message +func (c *CLIHandler) CreateMessage(ctx context.Context, input *types.MessageRequest) (*types.MessageResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() + + switch input.Body.Type { + case types.MessageTypeUser: + if err := c.conversation.SendMessage(FormatMessage(c.agentType, input.Body.Content)...); err != nil { + return nil, xerrors.Errorf("failed to send message: %w", err) + } + case types.MessageTypeRaw: + if _, err := c.agentio.Write([]byte(input.Body.Content)); err != nil { + return nil, xerrors.Errorf("failed to send message: %w", err) + } + } + + resp := &types.MessageResponse{} + resp.Body.Ok = true + + return resp, nil +} + +// SubscribeEvents is an SSE endpoint that sends events to the client +func (c *CLIHandler) SubscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) { + subscriberId, ch, stateEvents := c.emitter.Subscribe() + defer c.emitter.Unsubscribe(subscriberId) + c.logger.Info("New subscriber", "subscriberId", subscriberId) + for _, event := range stateEvents { + if event.Type == EventTypeScreenUpdate { + continue + } + if err := send.Data(event.Payload); err != nil { + c.logger.Error("Failed to send event", "subscriberId", subscriberId, "error", err) + return + } + } + for { + select { + case event, ok := <-ch: + if !ok { + c.logger.Info("Channel closed", "subscriberId", subscriberId) + return + } + if event.Type == EventTypeScreenUpdate { + continue + } + if err := send.Data(event.Payload); err != nil { + c.logger.Error("Failed to send event", "subscriberId", subscriberId, "error", err) + return + } + case <-ctx.Done(): + c.logger.Info("Context done", "subscriberId", subscriberId) + return + } + } +} + +func (c *CLIHandler) SubscribeConversations(ctx context.Context, input *struct{}, send sse.Sender) { + subscriberId, ch, stateEvents := c.emitter.Subscribe() + defer c.emitter.Unsubscribe(subscriberId) + c.logger.Info("New screen subscriber", "subscriberId", subscriberId) + for _, event := range stateEvents { + if event.Type != EventTypeScreenUpdate { + continue + } + if err := send.Data(event.Payload); err != nil { + c.logger.Error("Failed to send screen event", "subscriberId", subscriberId, "error", err) + return + } + } + for { + select { + case event, ok := <-ch: + if !ok { + c.logger.Info("Screen channel closed", "subscriberId", subscriberId) + return + } + if event.Type != EventTypeScreenUpdate { + continue + } + if err := send.Data(event.Payload); err != nil { + c.logger.Error("Failed to send screen event", "subscriberId", subscriberId, "error", err) + return + } + case <-ctx.Done(): + c.logger.Info("Screen context done", "subscriberId", subscriberId) + return + } + } +} + +func (c *CLIHandler) StartSnapshotLoop(ctx context.Context) { + c.conversation.StartSnapshotLoop(ctx) + go func() { + for { + c.emitter.UpdateStatusAndEmitChanges(c.conversation.Status()) + c.emitter.UpdateMessagesAndEmitChanges(c.conversation.Messages()) + c.emitter.UpdateScreenAndEmitChanges(c.conversation.Screen()) + time.Sleep(snapshotInterval) + } + }() +} diff --git a/lib/httpapi/events.go b/lib/cli/events.go similarity index 71% rename from lib/httpapi/events.go rename to lib/cli/events.go index 1e6281d..669f0a0 100644 --- a/lib/httpapi/events.go +++ b/lib/cli/events.go @@ -1,15 +1,13 @@ -package httpapi +package cli import ( - "fmt" "strings" "sync" "time" - mf "github.com/coder/agentapi/lib/msgfmt" - st "github.com/coder/agentapi/lib/screentracker" - "github.com/coder/agentapi/lib/util" - "github.com/danielgtaylor/huma/v2" + mf "github.com/coder/agentapi/lib/cli/msgfmt" + st "github.com/coder/agentapi/lib/cli/screentracker" + "github.com/coder/agentapi/lib/types" ) type EventType string @@ -20,31 +18,15 @@ const ( EventTypeScreenUpdate EventType = "screen_update" ) -type AgentStatus string - -const ( - AgentStatusRunning AgentStatus = "running" - AgentStatusStable AgentStatus = "stable" -) - -var AgentStatusValues = []AgentStatus{ - AgentStatusStable, - AgentStatusRunning, -} - -func (a AgentStatus) Schema(r huma.Registry) *huma.Schema { - return util.OpenAPISchema(r, "AgentStatus", AgentStatusValues) -} - type MessageUpdateBody struct { - Id int `json:"id" doc:"Unique identifier for the message. This identifier also represents the order of the message in the conversation history."` - Role st.ConversationRole `json:"role" doc:"Role of the message author"` - Message string `json:"message" doc:"Message content. The message is formatted as it appears in the agent's terminal session, meaning that, by default, it consists of lines of text with 80 characters per line."` - Time time.Time `json:"time" doc:"Timestamp of the message"` + Id int `json:"id" doc:"Unique identifier for the message. This identifier also represents the order of the message in the conversation history."` + Role types.ConversationRole `json:"role" doc:"Role of the message author"` + Message string `json:"message" doc:"Message content. The message is formatted as it appears in the agent's terminal session, meaning that, by default, it consists of lines of text with 80 characters per line."` + Time time.Time `json:"time" doc:"Timestamp of the message"` } type StatusChangeBody struct { - Status AgentStatus `json:"status" doc:"Agent status"` + Status types.AgentStatus `json:"status" doc:"Agent status"` } type ScreenUpdateBody struct { @@ -58,27 +40,14 @@ type Event struct { type EventEmitter struct { mu sync.Mutex - messages []st.ConversationMessage - status AgentStatus + messages []types.ConversationMessage + status types.AgentStatus chans map[int]chan Event chanIdx int subscriptionBufSize int screen string } -func convertStatus(status st.ConversationStatus) AgentStatus { - switch status { - case st.ConversationStatusInitializing: - return AgentStatusRunning - case st.ConversationStatusStable: - return AgentStatusStable - case st.ConversationStatusChanging: - return AgentStatusRunning - default: - panic(fmt.Sprintf("unknown conversation status: %s", status)) - } -} - // subscriptionBufSize is the size of the buffer for each subscription. // Once the buffer is full, the channel will be closed. // Listeners must actively drain the channel, so it's important to @@ -87,8 +56,8 @@ func convertStatus(status st.ConversationStatus) AgentStatus { func NewEventEmitter(subscriptionBufSize int) *EventEmitter { return &EventEmitter{ mu: sync.Mutex{}, - messages: make([]st.ConversationMessage, 0), - status: AgentStatusRunning, + messages: make([]types.ConversationMessage, 0), + status: types.AgentStatusRunning, chans: make(map[int]chan Event), chanIdx: 0, subscriptionBufSize: subscriptionBufSize, @@ -120,14 +89,14 @@ func (e *EventEmitter) notifyChannels(eventType EventType, payload any) { // Assumes that only the last message can change or new messages can be added. // If a new message is injected between existing messages (identified by Id), the behavior is undefined. -func (e *EventEmitter) UpdateMessagesAndEmitChanges(newMessages []st.ConversationMessage) { +func (e *EventEmitter) UpdateMessagesAndEmitChanges(newMessages []types.ConversationMessage) { e.mu.Lock() defer e.mu.Unlock() maxLength := max(len(e.messages), len(newMessages)) for i := range maxLength { - var oldMsg st.ConversationMessage - var newMsg st.ConversationMessage + var oldMsg types.ConversationMessage + var newMsg types.ConversationMessage if i < len(e.messages) { oldMsg = e.messages[i] } diff --git a/lib/cli/events_test.go b/lib/cli/events_test.go new file mode 100644 index 0000000..99417f0 --- /dev/null +++ b/lib/cli/events_test.go @@ -0,0 +1,102 @@ +package cli + +// +//import ( +// "fmt" +// "testing" +// "time" +// +// st "github.com/coder/agentapi/lib/cli/screentracker" +// "github.com/coder/agentapi/lib/httpapi" +// "github.com/stretchr/testify/assert" +//) +// +//func TestEventEmitter(t *testing.T) { +// t.Run("single-subscription", func(t *testing.T) { +// emitter := httpapi.NewEventEmitter(10) +// _, ch, stateEvents := emitter.Subscribe() +// assert.Empty(t, ch) +// assert.Equal(t, []httpapi.Event{ +// { +// Type: httpapi.EventTypeStatusChange, +// Payload: httpapi.StatusChangeBody{Status: httpapi.AgentStatusRunning}, +// }, +// { +// Type: httpapi.EventTypeScreenUpdate, +// Payload: httpapi.ConversationUpdateBody{Screen: ""}, +// }, +// }, stateEvents) +// +// now := time.Now() +// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ +// {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, +// }) +// newEvent := <-ch +// assert.Equal(t, httpapi.Event{ +// Type: httpapi.EventTypeMessageUpdate, +// Payload: httpapi.MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, +// }, newEvent) +// +// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ +// {Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, +// {Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, +// }) +// newEvent = <-ch +// assert.Equal(t, httpapi.Event{ +// Type: httpapi.EventTypeMessageUpdate, +// Payload: httpapi.MessageUpdateBody{Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, +// }, newEvent) +// +// newEvent = <-ch +// assert.Equal(t, httpapi.Event{ +// Type: httpapi.EventTypeMessageUpdate, +// Payload: httpapi.MessageUpdateBody{Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, +// }, newEvent) +// +// emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable) +// newEvent = <-ch +// assert.Equal(t, httpapi.Event{ +// Type: httpapi.EventTypeStatusChange, +// Payload: httpapi.StatusChangeBody{Status: httpapi.AgentStatusStable}, +// }, newEvent) +// }) +// +// t.Run("multiple-subscriptions", func(t *testing.T) { +// emitter := httpapi.NewEventEmitter(10) +// channels := make([]<-chan httpapi.Event, 0, 10) +// for i := 0; i < 10; i++ { +// _, ch, _ := emitter.Subscribe() +// channels = append(channels, ch) +// } +// now := time.Now() +// +// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ +// {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, +// }) +// for _, ch := range channels { +// newEvent := <-ch +// assert.Equal(t, httpapi.Event{ +// Type: httpapi.EventTypeMessageUpdate, +// Payload: httpapi.MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, +// }, newEvent) +// } +// }) +// +// t.Run("close-channel", func(t *testing.T) { +// emitter := httpapi.NewEventEmitter(1) +// _, ch, _ := emitter.Subscribe() +// for i := range 5 { +// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ +// {Id: i, Message: fmt.Sprintf("Hello, world! %d", i), Role: st.ConversationRoleUser, Time: time.Now()}, +// }) +// } +// _, ok := <-ch +// assert.True(t, ok) +// select { +// case _, ok := <-ch: +// assert.False(t, ok) +// default: +// t.Fatalf("read should not block") +// } +// }) +//} diff --git a/lib/msgfmt/message_box.go b/lib/cli/msgfmt/message_box.go similarity index 100% rename from lib/msgfmt/message_box.go rename to lib/cli/msgfmt/message_box.go diff --git a/lib/msgfmt/msgfmt.go b/lib/cli/msgfmt/msgfmt.go similarity index 95% rename from lib/msgfmt/msgfmt.go rename to lib/cli/msgfmt/msgfmt.go index 4844b3f..860c3d1 100644 --- a/lib/msgfmt/msgfmt.go +++ b/lib/cli/msgfmt/msgfmt.go @@ -232,17 +232,17 @@ func trimEmptyLines(message string) string { type AgentType string const ( - AgentTypeClaude AgentType = "claude" - AgentTypeGoose AgentType = "goose" - AgentTypeAider AgentType = "aider" - AgentTypeCodex AgentType = "codex" - AgentTypeGemini AgentType = "gemini" - AgentTypeAmp AgentType = "amp" - AgentTypeCursor AgentType = "cursor" - AgentTypeAuggie AgentType = "auggie" - AgentTypeAmazonQ AgentType = "amazonq" - AgentTypeOpencode AgentType = "opencode" - AgentTypeCustom AgentType = "custom" + AgentTypeClaude AgentType = "claude" + AgentTypeGoose AgentType = "goose" + AgentTypeAider AgentType = "aider" + AgentTypeCodex AgentType = "codex" + AgentTypeGemini AgentType = "gemini" + AgentTypeAmp AgentType = "amp" + AgentTypeCursor AgentType = "cursor" + AgentTypeAuggie AgentType = "auggie" + AgentTypeAmazonQ AgentType = "amazonq" + AgentTypeOpencode AgentType = "opencode" + AgentTypeCustom AgentType = "custom" ) func formatGenericMessage(message string, userInput string, agentType AgentType) string { diff --git a/lib/msgfmt/msgfmt_test.go b/lib/cli/msgfmt/msgfmt_test.go similarity index 100% rename from lib/msgfmt/msgfmt_test.go rename to lib/cli/msgfmt/msgfmt_test.go diff --git a/lib/msgfmt/testdata/format/aider/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/aider/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/aider/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/aider/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/aider/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/aider/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/aider/first_message/user.txt b/lib/cli/msgfmt/testdata/format/aider/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/aider/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/aider/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/aider/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/aider/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/aider/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/aider/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/aider/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/aider/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/aider/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/aider/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/aider/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/aider/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/aider/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/aider/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/aider/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/aider/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/aider/second_message/user.txt b/lib/cli/msgfmt/testdata/format/aider/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/aider/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/aider/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/amazonq/confirmation_box/expected.txt b/lib/cli/msgfmt/testdata/format/amazonq/confirmation_box/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/confirmation_box/expected.txt rename to lib/cli/msgfmt/testdata/format/amazonq/confirmation_box/expected.txt diff --git a/lib/msgfmt/testdata/format/amazonq/confirmation_box/msg.txt b/lib/cli/msgfmt/testdata/format/amazonq/confirmation_box/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/confirmation_box/msg.txt rename to lib/cli/msgfmt/testdata/format/amazonq/confirmation_box/msg.txt diff --git a/lib/msgfmt/testdata/format/amazonq/confirmation_box/user.txt b/lib/cli/msgfmt/testdata/format/amazonq/confirmation_box/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/confirmation_box/user.txt rename to lib/cli/msgfmt/testdata/format/amazonq/confirmation_box/user.txt diff --git a/lib/msgfmt/testdata/format/amazonq/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/amazonq/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/amazonq/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/amazonq/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/amazonq/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/amazonq/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/amazonq/first_message/user.txt b/lib/cli/msgfmt/testdata/format/amazonq/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/amazonq/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/amazonq/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/amazonq/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/amazonq/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/amazonq/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/amazonq/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/amazonq/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/amazonq/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/amazonq/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/amazonq/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/amazonq/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/amazonq/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/amazonq/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/amazonq/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/amazonq/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/amazonq/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/amazonq/second_message/user.txt b/lib/cli/msgfmt/testdata/format/amazonq/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/amazonq/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/amazonq/thinking/expected.txt b/lib/cli/msgfmt/testdata/format/amazonq/thinking/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/thinking/expected.txt rename to lib/cli/msgfmt/testdata/format/amazonq/thinking/expected.txt diff --git a/lib/msgfmt/testdata/format/amazonq/thinking/msg.txt b/lib/cli/msgfmt/testdata/format/amazonq/thinking/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/thinking/msg.txt rename to lib/cli/msgfmt/testdata/format/amazonq/thinking/msg.txt diff --git a/lib/msgfmt/testdata/format/amazonq/thinking/user.txt b/lib/cli/msgfmt/testdata/format/amazonq/thinking/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amazonq/thinking/user.txt rename to lib/cli/msgfmt/testdata/format/amazonq/thinking/user.txt diff --git a/lib/msgfmt/testdata/format/amp/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/amp/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/amp/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/amp/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/amp/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/amp/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/amp/first_message/user.txt b/lib/cli/msgfmt/testdata/format/amp/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/amp/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/amp/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/amp/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/amp/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/amp/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/amp/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/amp/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/amp/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/amp/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/amp/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/amp/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/amp/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/amp/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/amp/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/amp/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/amp/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/amp/second_message/user.txt b/lib/cli/msgfmt/testdata/format/amp/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/amp/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/amp/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/auggie/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/auggie/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/auggie/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/auggie/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/auggie/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/auggie/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/auggie/first_message/user.txt b/lib/cli/msgfmt/testdata/format/auggie/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/auggie/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/auggie/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/auggie/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/auggie/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/auggie/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/auggie/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/auggie/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/auggie/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/auggie/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/auggie/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/auggie/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/auggie/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/auggie/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/auggie/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/auggie/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/auggie/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/auggie/second_message/user.txt b/lib/cli/msgfmt/testdata/format/auggie/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/auggie/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/auggie/thinking/expected.txt b/lib/cli/msgfmt/testdata/format/auggie/thinking/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/thinking/expected.txt rename to lib/cli/msgfmt/testdata/format/auggie/thinking/expected.txt diff --git a/lib/msgfmt/testdata/format/auggie/thinking/msg.txt b/lib/cli/msgfmt/testdata/format/auggie/thinking/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/thinking/msg.txt rename to lib/cli/msgfmt/testdata/format/auggie/thinking/msg.txt diff --git a/lib/msgfmt/testdata/format/auggie/thinking/user.txt b/lib/cli/msgfmt/testdata/format/auggie/thinking/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/auggie/thinking/user.txt rename to lib/cli/msgfmt/testdata/format/auggie/thinking/user.txt diff --git a/lib/msgfmt/testdata/format/claude/auto-accept-edits/expected.txt b/lib/cli/msgfmt/testdata/format/claude/auto-accept-edits/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/auto-accept-edits/expected.txt rename to lib/cli/msgfmt/testdata/format/claude/auto-accept-edits/expected.txt diff --git a/lib/msgfmt/testdata/format/claude/auto-accept-edits/msg.txt b/lib/cli/msgfmt/testdata/format/claude/auto-accept-edits/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/auto-accept-edits/msg.txt rename to lib/cli/msgfmt/testdata/format/claude/auto-accept-edits/msg.txt diff --git a/lib/msgfmt/testdata/format/claude/auto-accept-edits/user.txt b/lib/cli/msgfmt/testdata/format/claude/auto-accept-edits/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/auto-accept-edits/user.txt rename to lib/cli/msgfmt/testdata/format/claude/auto-accept-edits/user.txt diff --git a/lib/msgfmt/testdata/format/claude/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/claude/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/claude/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/claude/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/claude/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/claude/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/claude/first_message/user.txt b/lib/cli/msgfmt/testdata/format/claude/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/claude/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/claude/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/claude/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/claude/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/claude/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/claude/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/claude/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/claude/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/claude/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/claude/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/claude/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/claude/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/claude/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/claude/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/claude/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/claude/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/claude/second_message/user.txt b/lib/cli/msgfmt/testdata/format/claude/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/claude/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/claude/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/codex/confirmation_box/expected.txt b/lib/cli/msgfmt/testdata/format/codex/confirmation_box/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/confirmation_box/expected.txt rename to lib/cli/msgfmt/testdata/format/codex/confirmation_box/expected.txt diff --git a/lib/msgfmt/testdata/format/codex/confirmation_box/msg.txt b/lib/cli/msgfmt/testdata/format/codex/confirmation_box/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/confirmation_box/msg.txt rename to lib/cli/msgfmt/testdata/format/codex/confirmation_box/msg.txt diff --git a/lib/msgfmt/testdata/format/codex/confirmation_box/user.txt b/lib/cli/msgfmt/testdata/format/codex/confirmation_box/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/confirmation_box/user.txt rename to lib/cli/msgfmt/testdata/format/codex/confirmation_box/user.txt diff --git a/lib/msgfmt/testdata/format/codex/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/codex/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/codex/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/codex/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/codex/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/codex/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/codex/first_message/user.txt b/lib/cli/msgfmt/testdata/format/codex/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/codex/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/codex/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/codex/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/codex/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/codex/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/codex/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/codex/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/codex/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/codex/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/codex/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/codex/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/codex/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/codex/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/codex/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/codex/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/codex/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/codex/second_message/user.txt b/lib/cli/msgfmt/testdata/format/codex/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/codex/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/codex/thinking/expected.txt b/lib/cli/msgfmt/testdata/format/codex/thinking/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/thinking/expected.txt rename to lib/cli/msgfmt/testdata/format/codex/thinking/expected.txt diff --git a/lib/msgfmt/testdata/format/codex/thinking/msg.txt b/lib/cli/msgfmt/testdata/format/codex/thinking/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/thinking/msg.txt rename to lib/cli/msgfmt/testdata/format/codex/thinking/msg.txt diff --git a/lib/msgfmt/testdata/format/codex/thinking/user.txt b/lib/cli/msgfmt/testdata/format/codex/thinking/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/codex/thinking/user.txt rename to lib/cli/msgfmt/testdata/format/codex/thinking/user.txt diff --git a/lib/msgfmt/testdata/format/cursor/confirmation_box/expected.txt b/lib/cli/msgfmt/testdata/format/cursor/confirmation_box/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/confirmation_box/expected.txt rename to lib/cli/msgfmt/testdata/format/cursor/confirmation_box/expected.txt diff --git a/lib/msgfmt/testdata/format/cursor/confirmation_box/msg.txt b/lib/cli/msgfmt/testdata/format/cursor/confirmation_box/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/confirmation_box/msg.txt rename to lib/cli/msgfmt/testdata/format/cursor/confirmation_box/msg.txt diff --git a/lib/msgfmt/testdata/format/cursor/confirmation_box/user.txt b/lib/cli/msgfmt/testdata/format/cursor/confirmation_box/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/confirmation_box/user.txt rename to lib/cli/msgfmt/testdata/format/cursor/confirmation_box/user.txt diff --git a/lib/msgfmt/testdata/format/cursor/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/cursor/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/cursor/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/cursor/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/cursor/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/cursor/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/cursor/first_message/user.txt b/lib/cli/msgfmt/testdata/format/cursor/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/cursor/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/cursor/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/cursor/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/cursor/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/cursor/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/cursor/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/cursor/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/cursor/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/cursor/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/cursor/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/cursor/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/cursor/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/cursor/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/cursor/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/cursor/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/cursor/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/cursor/second_message/user.txt b/lib/cli/msgfmt/testdata/format/cursor/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/cursor/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/cursor/thinking/expected.txt b/lib/cli/msgfmt/testdata/format/cursor/thinking/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/thinking/expected.txt rename to lib/cli/msgfmt/testdata/format/cursor/thinking/expected.txt diff --git a/lib/msgfmt/testdata/format/cursor/thinking/msg.txt b/lib/cli/msgfmt/testdata/format/cursor/thinking/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/thinking/msg.txt rename to lib/cli/msgfmt/testdata/format/cursor/thinking/msg.txt diff --git a/lib/msgfmt/testdata/format/cursor/thinking/user.txt b/lib/cli/msgfmt/testdata/format/cursor/thinking/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/cursor/thinking/user.txt rename to lib/cli/msgfmt/testdata/format/cursor/thinking/user.txt diff --git a/lib/msgfmt/testdata/format/gemini/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/gemini/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/gemini/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/gemini/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/gemini/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/gemini/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/gemini/first_message/user.txt b/lib/cli/msgfmt/testdata/format/gemini/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/gemini/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/gemini/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/gemini/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/gemini/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/gemini/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/gemini/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/gemini/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/gemini/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/gemini/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/gemini/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/gemini/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/gemini/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/gemini/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/gemini/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/gemini/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/gemini/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/gemini/second_message/user.txt b/lib/cli/msgfmt/testdata/format/gemini/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/gemini/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/gemini/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/goose/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/goose/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/goose/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/goose/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/goose/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/goose/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/goose/first_message/user.txt b/lib/cli/msgfmt/testdata/format/goose/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/goose/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/goose/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/goose/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/goose/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/goose/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/goose/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/goose/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/goose/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/goose/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/goose/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/goose/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/goose/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/goose/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/goose/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/goose/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/goose/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/goose/second_message/user.txt b/lib/cli/msgfmt/testdata/format/goose/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/goose/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/goose/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/opencode/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/opencode/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/opencode/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/opencode/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/opencode/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/opencode/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/opencode/first_message/user.txt b/lib/cli/msgfmt/testdata/format/opencode/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/opencode/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/opencode/second_message/expected.txt b/lib/cli/msgfmt/testdata/format/opencode/second_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/second_message/expected.txt rename to lib/cli/msgfmt/testdata/format/opencode/second_message/expected.txt diff --git a/lib/msgfmt/testdata/format/opencode/second_message/msg.txt b/lib/cli/msgfmt/testdata/format/opencode/second_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/second_message/msg.txt rename to lib/cli/msgfmt/testdata/format/opencode/second_message/msg.txt diff --git a/lib/msgfmt/testdata/format/opencode/second_message/user.txt b/lib/cli/msgfmt/testdata/format/opencode/second_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/second_message/user.txt rename to lib/cli/msgfmt/testdata/format/opencode/second_message/user.txt diff --git a/lib/msgfmt/testdata/format/opencode/thinking/expected.txt b/lib/cli/msgfmt/testdata/format/opencode/thinking/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/thinking/expected.txt rename to lib/cli/msgfmt/testdata/format/opencode/thinking/expected.txt diff --git a/lib/msgfmt/testdata/format/opencode/thinking/msg.txt b/lib/cli/msgfmt/testdata/format/opencode/thinking/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/thinking/msg.txt rename to lib/cli/msgfmt/testdata/format/opencode/thinking/msg.txt diff --git a/lib/msgfmt/testdata/format/opencode/thinking/user.txt b/lib/cli/msgfmt/testdata/format/opencode/thinking/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/opencode/thinking/user.txt rename to lib/cli/msgfmt/testdata/format/opencode/thinking/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/aider/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/aider/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/aider/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/aider/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/aider/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/aider/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/aider/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/aider/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/aider/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/aider/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/aider/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/aider/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/claude/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/claude/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/claude/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/claude/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/claude/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/claude/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/claude/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/claude/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/claude/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/claude/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/claude/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/claude/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/empty-input/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/empty-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/empty-input/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/empty-input/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/empty-input/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/empty-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/empty-input/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/empty-input/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/empty-input/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/empty-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/empty-input/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/empty-input/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/full-match/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/full-match/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/full-match/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/full-match/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/full-match/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/full-match/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/full-match/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/full-match/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/full-match/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/full-match/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/full-match/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/full-match/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/goose/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/goose/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/goose/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/goose/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/goose/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/goose/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/goose/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/goose/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/goose/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/goose/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/goose/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/goose/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/no-user-input-in-message/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/no-user-input-in-message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/no-user-input-in-message/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/no-user-input-in-message/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/no-user-input-in-message/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/no-user-input-in-message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/no-user-input-in-message/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/no-user-input-in-message/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/no-user-input-in-message/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/no-user-input-in-message/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/no-user-input-in-message/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/no-user-input-in-message/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii-2/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii-2/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii-2/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii-2/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii-2/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii-2/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii-2/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii-2/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii-2/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii-2/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii-2/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii-2/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii-3/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii-3/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii-3/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii-3/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii-3/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii-3/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii-3/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii-3/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii-3/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii-3/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii-3/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii-3/user.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii/expected.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii/expected.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii/expected.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii/expected.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii/msg.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii/msg.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii/msg.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii/msg.txt diff --git a/lib/msgfmt/testdata/remove-user-input/non-ascii/user.txt b/lib/cli/msgfmt/testdata/remove-user-input/non-ascii/user.txt similarity index 100% rename from lib/msgfmt/testdata/remove-user-input/non-ascii/user.txt rename to lib/cli/msgfmt/testdata/remove-user-input/non-ascii/user.txt diff --git a/lib/screentracker/conversation.go b/lib/cli/screentracker/conversation.go similarity index 92% rename from lib/screentracker/conversation.go rename to lib/cli/screentracker/conversation.go index 7777e04..ebc47db 100644 --- a/lib/screentracker/conversation.go +++ b/lib/cli/screentracker/conversation.go @@ -7,9 +7,9 @@ import ( "sync" "time" - "github.com/coder/agentapi/lib/msgfmt" + "github.com/coder/agentapi/lib/cli/msgfmt" + "github.com/coder/agentapi/lib/types" "github.com/coder/agentapi/lib/util" - "github.com/danielgtaylor/huma/v2" "golang.org/x/xerrors" ) @@ -43,35 +43,17 @@ type ConversationConfig struct { SkipSendMessageStatusCheck bool } -type ConversationRole string - const ( - ConversationRoleUser ConversationRole = "user" - ConversationRoleAgent ConversationRole = "agent" + ConversationRoleUser = types.ConversationRoleUser + ConversationRoleAgent = types.ConversationRoleAgent ) -var ConversationRoleValues = []ConversationRole{ - ConversationRoleUser, - ConversationRoleAgent, -} - -func (c ConversationRole) Schema(r huma.Registry) *huma.Schema { - return util.OpenAPISchema(r, "ConversationRole", ConversationRoleValues) -} - -type ConversationMessage struct { - Id int - Message string - Role ConversationRole - Time time.Time -} - type Conversation struct { cfg ConversationConfig // How many stable snapshots are required to consider the screen stable stableSnapshotsThreshold int snapshotBuffer *RingBuffer[screenSnapshot] - messages []ConversationMessage + messages []types.ConversationMessage screenBeforeLastUserMessage string lock sync.Mutex } @@ -100,7 +82,7 @@ func NewConversation(ctx context.Context, cfg ConversationConfig) *Conversation cfg: cfg, stableSnapshotsThreshold: threshold, snapshotBuffer: NewRingBuffer[screenSnapshot](threshold), - messages: []ConversationMessage{ + messages: []types.ConversationMessage{ { Message: "", Role: ConversationRoleAgent, @@ -182,13 +164,13 @@ func FindNewMessage(oldScreen, newScreen string, agentType msgfmt.AgentType) str return strings.Join(newSectionLines[startLine:endLine+1], "\n") } -func (c *Conversation) lastMessage(role ConversationRole) ConversationMessage { +func (c *Conversation) lastMessage(role types.ConversationRole) types.ConversationMessage { for i := len(c.messages) - 1; i >= 0; i-- { if c.messages[i].Role == role { return c.messages[i] } } - return ConversationMessage{} + return types.ConversationMessage{} } // This function assumes that the caller holds the lock @@ -203,7 +185,7 @@ func (c *Conversation) updateLastAgentMessage(screen string, timestamp time.Time if lastAgentMessage.Message == agentMessage { return } - conversationMessage := ConversationMessage{ + conversationMessage := types.ConversationMessage{ Message: agentMessage, Role: ConversationRoleAgent, Time: timestamp, @@ -359,7 +341,7 @@ func (c *Conversation) SendMessage(messageParts ...MessagePart) error { } c.screenBeforeLastUserMessage = screenBeforeMessage - c.messages = append(c.messages, ConversationMessage{ + c.messages = append(c.messages, types.ConversationMessage{ Id: len(c.messages), Message: message, Role: ConversationRoleUser, @@ -405,11 +387,11 @@ func (c *Conversation) Status() ConversationStatus { return c.statusInner() } -func (c *Conversation) Messages() []ConversationMessage { +func (c *Conversation) Messages() []types.ConversationMessage { c.lock.Lock() defer c.lock.Unlock() - result := make([]ConversationMessage, len(c.messages)) + result := make([]types.ConversationMessage, len(c.messages)) copy(result, c.messages) return result } diff --git a/lib/screentracker/conversation_test.go b/lib/cli/screentracker/conversation_test.go similarity index 99% rename from lib/screentracker/conversation_test.go rename to lib/cli/screentracker/conversation_test.go index 53c77fd..1561400 100644 --- a/lib/screentracker/conversation_test.go +++ b/lib/cli/screentracker/conversation_test.go @@ -8,10 +8,9 @@ import ( "testing" "time" - "github.com/coder/agentapi/lib/msgfmt" + "github.com/coder/agentapi/lib/cli/msgfmt" + st "github.com/coder/agentapi/lib/cli/screentracker" "github.com/stretchr/testify/assert" - - st "github.com/coder/agentapi/lib/screentracker" ) type statusTestStep struct { diff --git a/lib/screentracker/ringbuffer.go b/lib/cli/screentracker/ringbuffer.go similarity index 100% rename from lib/screentracker/ringbuffer.go rename to lib/cli/screentracker/ringbuffer.go diff --git a/lib/screentracker/testdata/diff/basic/after.txt b/lib/cli/screentracker/testdata/diff/basic/after.txt similarity index 100% rename from lib/screentracker/testdata/diff/basic/after.txt rename to lib/cli/screentracker/testdata/diff/basic/after.txt diff --git a/lib/screentracker/testdata/diff/basic/before.txt b/lib/cli/screentracker/testdata/diff/basic/before.txt similarity index 100% rename from lib/screentracker/testdata/diff/basic/before.txt rename to lib/cli/screentracker/testdata/diff/basic/before.txt diff --git a/lib/screentracker/testdata/diff/basic/expected.txt b/lib/cli/screentracker/testdata/diff/basic/expected.txt similarity index 100% rename from lib/screentracker/testdata/diff/basic/expected.txt rename to lib/cli/screentracker/testdata/diff/basic/expected.txt diff --git a/lib/screentracker/testdata/diff/no-change/after.txt b/lib/cli/screentracker/testdata/diff/no-change/after.txt similarity index 100% rename from lib/screentracker/testdata/diff/no-change/after.txt rename to lib/cli/screentracker/testdata/diff/no-change/after.txt diff --git a/lib/screentracker/testdata/diff/no-change/before.txt b/lib/cli/screentracker/testdata/diff/no-change/before.txt similarity index 100% rename from lib/screentracker/testdata/diff/no-change/before.txt rename to lib/cli/screentracker/testdata/diff/no-change/before.txt diff --git a/lib/screentracker/testdata/diff/no-change/expected.txt b/lib/cli/screentracker/testdata/diff/no-change/expected.txt similarity index 100% rename from lib/screentracker/testdata/diff/no-change/expected.txt rename to lib/cli/screentracker/testdata/diff/no-change/expected.txt diff --git a/lib/termexec/termexec.go b/lib/cli/termexec/termexec.go similarity index 100% rename from lib/termexec/termexec.go rename to lib/cli/termexec/termexec.go diff --git a/lib/httpapi/events_test.go b/lib/httpapi/events_test.go deleted file mode 100644 index 23a1d36..0000000 --- a/lib/httpapi/events_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package httpapi - -import ( - "fmt" - "testing" - "time" - - st "github.com/coder/agentapi/lib/screentracker" - "github.com/stretchr/testify/assert" -) - -func TestEventEmitter(t *testing.T) { - t.Run("single-subscription", func(t *testing.T) { - emitter := NewEventEmitter(10) - _, ch, stateEvents := emitter.Subscribe() - assert.Empty(t, ch) - assert.Equal(t, []Event{ - { - Type: EventTypeStatusChange, - Payload: StatusChangeBody{Status: AgentStatusRunning}, - }, - { - Type: EventTypeScreenUpdate, - Payload: ScreenUpdateBody{Screen: ""}, - }, - }, stateEvents) - - now := time.Now() - emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ - {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, - }) - newEvent := <-ch - assert.Equal(t, Event{ - Type: EventTypeMessageUpdate, - Payload: MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, - }, newEvent) - - emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ - {Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, - {Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, - }) - newEvent = <-ch - assert.Equal(t, Event{ - Type: EventTypeMessageUpdate, - Payload: MessageUpdateBody{Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, - }, newEvent) - - newEvent = <-ch - assert.Equal(t, Event{ - Type: EventTypeMessageUpdate, - Payload: MessageUpdateBody{Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, - }, newEvent) - - emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable) - newEvent = <-ch - assert.Equal(t, Event{ - Type: EventTypeStatusChange, - Payload: StatusChangeBody{Status: AgentStatusStable}, - }, newEvent) - }) - - t.Run("multiple-subscriptions", func(t *testing.T) { - emitter := NewEventEmitter(10) - channels := make([]<-chan Event, 0, 10) - for i := 0; i < 10; i++ { - _, ch, _ := emitter.Subscribe() - channels = append(channels, ch) - } - now := time.Now() - - emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ - {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, - }) - for _, ch := range channels { - newEvent := <-ch - assert.Equal(t, Event{ - Type: EventTypeMessageUpdate, - Payload: MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, - }, newEvent) - } - }) - - t.Run("close-channel", func(t *testing.T) { - emitter := NewEventEmitter(1) - _, ch, _ := emitter.Subscribe() - for i := range 5 { - emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ - {Id: i, Message: fmt.Sprintf("Hello, world! %d", i), Role: st.ConversationRoleUser, Time: time.Now()}, - }) - } - _, ok := <-ch - assert.True(t, ok) - select { - case _, ok := <-ch: - assert.False(t, ok) - default: - t.Fatalf("read should not block") - } - }) -} diff --git a/lib/httpapi/handler.go b/lib/httpapi/handler.go new file mode 100644 index 0000000..f162bd5 --- /dev/null +++ b/lib/httpapi/handler.go @@ -0,0 +1,18 @@ +package httpapi + +import ( + "context" + + "github.com/coder/agentapi/lib/types" + "github.com/danielgtaylor/huma/v2/sse" +) + +// InteractionHandler defines the interface that all interaction modes must implement +type InteractionHandler interface { + GetStatus(ctx context.Context, input *struct{}) (*types.StatusResponse, error) + CreateMessage(ctx context.Context, input *types.MessageRequest) (*types.MessageResponse, error) + GetMessages(ctx context.Context, input *struct{}) (*types.MessagesResponse, error) + SubscribeConversations(ctx context.Context, input *struct{}, send sse.Sender) + SubscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) + //StartSnapshotLoop(ctx context.Context) +} diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index b72b4c3..fea80ac 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -9,15 +9,16 @@ import ( "net/url" "slices" "strings" - "sync" "time" "unicode" "github.com/coder/agentapi/internal/version" + "github.com/coder/agentapi/lib/cli" + "github.com/coder/agentapi/lib/cli/msgfmt" + "github.com/coder/agentapi/lib/cli/termexec" "github.com/coder/agentapi/lib/logctx" - mf "github.com/coder/agentapi/lib/msgfmt" - st "github.com/coder/agentapi/lib/screentracker" - "github.com/coder/agentapi/lib/termexec" + "github.com/coder/agentapi/lib/sdk" + "github.com/coder/agentapi/lib/types" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/sse" @@ -28,17 +29,14 @@ import ( // Server represents the HTTP server type Server struct { - router chi.Router - api huma.API - port int - srv *http.Server - mu sync.RWMutex - logger *slog.Logger - conversation *st.Conversation - agentio *termexec.Process - agentType mf.AgentType - emitter *EventEmitter - chatBasePath string + router chi.Router + api huma.API + port int + srv *http.Server + logger *slog.Logger + agentType msgfmt.AgentType + chatBasePath string + InteractionHandler InteractionHandler } func (s *Server) GetOpenAPI() string { @@ -63,12 +61,13 @@ func (s *Server) GetOpenAPI() string { const snapshotInterval = 25 * time.Millisecond type ServerConfig struct { - AgentType mf.AgentType - Process *termexec.Process - Port int - ChatBasePath string - AllowedHosts []string - AllowedOrigins []string + AgentType msgfmt.AgentType + InteractionType types.InteractionType + Process *termexec.Process + Port int + ChatBasePath string + AllowedHosts []string + AllowedOrigins []string } // Validate allowed hosts don't contain whitespace, commas, schemes, or ports. @@ -192,32 +191,24 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { humaConfig := huma.DefaultConfig("AgentAPI", version.Version) humaConfig.Info.Description = "HTTP API for Claude Code, Goose, and Aider.\n\nhttps://github.com/coder/agentapi" api := humachi.New(router, humaConfig) - formatMessage := func(message string, userInput string) string { - return mf.FormatAgentMessage(config.AgentType, message, userInput) - } - conversation := st.NewConversation(ctx, st.ConversationConfig{ - AgentType: config.AgentType, - AgentIO: config.Process, - GetTime: func() time.Time { - return time.Now() - }, - SnapshotInterval: snapshotInterval, - ScreenStabilityLength: 2 * time.Second, - FormatMessage: formatMessage, - }) - emitter := NewEventEmitter(1024) s := &Server{ router: router, api: api, port: config.Port, - conversation: conversation, logger: logger, - agentio: config.Process, agentType: config.AgentType, - emitter: emitter, chatBasePath: strings.TrimSuffix(config.ChatBasePath, "/"), } + // Get the appropriate Interaction Handler + if config.InteractionType == types.CLIInteractionType { + s.InteractionHandler = cli.NewCLIHandler(ctx, config.Process, config.AgentType) + } else if config.InteractionType == types.SDKInteractionType { + s.InteractionHandler = sdk.NewSDKHandler(ctx) + } else { + return nil, xerrors.Errorf("") + } + // Register API routes s.registerRoutes() @@ -263,32 +254,20 @@ func hostAuthorizationMiddleware(allowedHosts []string, badHostHandler http.Hand } } -func (s *Server) StartSnapshotLoop(ctx context.Context) { - s.conversation.StartSnapshotLoop(ctx) - go func() { - for { - s.emitter.UpdateStatusAndEmitChanges(s.conversation.Status()) - s.emitter.UpdateMessagesAndEmitChanges(s.conversation.Messages()) - s.emitter.UpdateScreenAndEmitChanges(s.conversation.Screen()) - time.Sleep(snapshotInterval) - } - }() -} - // registerRoutes sets up all API endpoints func (s *Server) registerRoutes() { // GET /status endpoint - huma.Get(s.api, "/status", s.getStatus, func(o *huma.Operation) { + huma.Get(s.api, "/status", s.InteractionHandler.GetStatus, func(o *huma.Operation) { o.Description = "Returns the current status of the agent." }) // GET /messages endpoint - huma.Get(s.api, "/messages", s.getMessages, func(o *huma.Operation) { + huma.Get(s.api, "/messages", s.InteractionHandler.GetMessages, func(o *huma.Operation) { o.Description = "Returns a list of messages representing the conversation history with the agent." }) // POST /message endpoint - huma.Post(s.api, "/message", s.createMessage, func(o *huma.Operation) { + huma.Post(s.api, "/message", s.InteractionHandler.CreateMessage, func(o *huma.Operation) { o.Description = "Send a message to the agent. For messages of type 'user', the agent's status must be 'stable' for the operation to complete successfully. Otherwise, this endpoint will return an error." }) @@ -301,9 +280,9 @@ func (s *Server) registerRoutes() { Description: "The events are sent as Server-Sent Events (SSE). Initially, the endpoint returns a list of events needed to reconstruct the current state of the conversation and the agent's status. After that, it only returns events that have occurred since the last event was sent.\n\nNote: When an agent is running, the last message in the conversation history is updated frequently, and the endpoint sends a new message update event each time.", }, map[string]any{ // Mapping of event type name to Go struct for that event. - "message_update": MessageUpdateBody{}, - "status_change": StatusChangeBody{}, - }, s.subscribeEvents) + "message_update": types.MessageUpdateBody{}, + "status_change": types.StatusChangeBody{}, + }, s.InteractionHandler.SubscribeEvents) sse.Register(s.api, huma.Operation{ OperationID: "subscribeScreen", @@ -312,8 +291,8 @@ func (s *Server) registerRoutes() { Summary: "Subscribe to screen", Hidden: true, }, map[string]any{ - "screen": ScreenUpdateBody{}, - }, s.subscribeScreen) + "screen": types.ScreenUpdateBody{}, + }, s.InteractionHandler.SubscribeConversations) s.router.Handle("/", http.HandlerFunc(s.redirectToChat)) @@ -321,130 +300,6 @@ func (s *Server) registerRoutes() { s.registerStaticFileRoutes() } -// getStatus handles GET /status -func (s *Server) getStatus(ctx context.Context, input *struct{}) (*StatusResponse, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - status := s.conversation.Status() - agentStatus := convertStatus(status) - - resp := &StatusResponse{} - resp.Body.Status = agentStatus - - return resp, nil -} - -// getMessages handles GET /messages -func (s *Server) getMessages(ctx context.Context, input *struct{}) (*MessagesResponse, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - resp := &MessagesResponse{} - resp.Body.Messages = make([]Message, len(s.conversation.Messages())) - for i, msg := range s.conversation.Messages() { - resp.Body.Messages[i] = Message{ - Id: msg.Id, - Role: msg.Role, - Content: msg.Message, - Time: msg.Time, - } - } - - return resp, nil -} - -// createMessage handles POST /message -func (s *Server) createMessage(ctx context.Context, input *MessageRequest) (*MessageResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - - switch input.Body.Type { - case MessageTypeUser: - if err := s.conversation.SendMessage(FormatMessage(s.agentType, input.Body.Content)...); err != nil { - return nil, xerrors.Errorf("failed to send message: %w", err) - } - case MessageTypeRaw: - if _, err := s.agentio.Write([]byte(input.Body.Content)); err != nil { - return nil, xerrors.Errorf("failed to send message: %w", err) - } - } - - resp := &MessageResponse{} - resp.Body.Ok = true - - return resp, nil -} - -// subscribeEvents is an SSE endpoint that sends events to the client -func (s *Server) subscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) { - subscriberId, ch, stateEvents := s.emitter.Subscribe() - defer s.emitter.Unsubscribe(subscriberId) - s.logger.Info("New subscriber", "subscriberId", subscriberId) - for _, event := range stateEvents { - if event.Type == EventTypeScreenUpdate { - continue - } - if err := send.Data(event.Payload); err != nil { - s.logger.Error("Failed to send event", "subscriberId", subscriberId, "error", err) - return - } - } - for { - select { - case event, ok := <-ch: - if !ok { - s.logger.Info("Channel closed", "subscriberId", subscriberId) - return - } - if event.Type == EventTypeScreenUpdate { - continue - } - if err := send.Data(event.Payload); err != nil { - s.logger.Error("Failed to send event", "subscriberId", subscriberId, "error", err) - return - } - case <-ctx.Done(): - s.logger.Info("Context done", "subscriberId", subscriberId) - return - } - } -} - -func (s *Server) subscribeScreen(ctx context.Context, input *struct{}, send sse.Sender) { - subscriberId, ch, stateEvents := s.emitter.Subscribe() - defer s.emitter.Unsubscribe(subscriberId) - s.logger.Info("New screen subscriber", "subscriberId", subscriberId) - for _, event := range stateEvents { - if event.Type != EventTypeScreenUpdate { - continue - } - if err := send.Data(event.Payload); err != nil { - s.logger.Error("Failed to send screen event", "subscriberId", subscriberId, "error", err) - return - } - } - for { - select { - case event, ok := <-ch: - if !ok { - s.logger.Info("Screen channel closed", "subscriberId", subscriberId) - return - } - if event.Type != EventTypeScreenUpdate { - continue - } - if err := send.Data(event.Payload); err != nil { - s.logger.Error("Failed to send screen event", "subscriberId", subscriberId, "error", err) - return - } - case <-ctx.Done(): - s.logger.Info("Screen context done", "subscriberId", subscriberId) - return - } - } -} - // Start starts the HTTP server func (s *Server) Start() error { addr := fmt.Sprintf(":%d", s.port) diff --git a/lib/httpapi/server_test.go b/lib/httpapi/server_test.go index bc50d3e..ca42796 100644 --- a/lib/httpapi/server_test.go +++ b/lib/httpapi/server_test.go @@ -12,9 +12,9 @@ import ( "sort" "testing" + "github.com/coder/agentapi/lib/cli/msgfmt" "github.com/coder/agentapi/lib/httpapi" "github.com/coder/agentapi/lib/logctx" - "github.com/coder/agentapi/lib/msgfmt" "github.com/stretchr/testify/require" ) diff --git a/lib/httpapi/setup.go b/lib/httpapi/setup.go index 1620304..f8ddb30 100644 --- a/lib/httpapi/setup.go +++ b/lib/httpapi/setup.go @@ -9,9 +9,9 @@ import ( "syscall" "time" + mf "github.com/coder/agentapi/lib/cli/msgfmt" + "github.com/coder/agentapi/lib/cli/termexec" "github.com/coder/agentapi/lib/logctx" - mf "github.com/coder/agentapi/lib/msgfmt" - "github.com/coder/agentapi/lib/termexec" ) type SetupProcessConfig struct { diff --git a/lib/sdk/agents/claude/claude.go b/lib/sdk/agents/claude/claude.go new file mode 100644 index 0000000..6e25b57 --- /dev/null +++ b/lib/sdk/agents/claude/claude.go @@ -0,0 +1 @@ +package claude diff --git a/lib/sdk/conversation.go b/lib/sdk/conversation.go new file mode 100644 index 0000000..26a30ac --- /dev/null +++ b/lib/sdk/conversation.go @@ -0,0 +1,9 @@ +package sdk + +import "github.com/coder/agentapi/lib/types" + +type SDK interface { + QueryAgent(userInput string) (string, error) + InitializeAgent(interface{}) error + GetStatus() (*types.StatusResponse, error) +} diff --git a/lib/sdk/sdk_handler.go b/lib/sdk/sdk_handler.go new file mode 100644 index 0000000..eaace6b --- /dev/null +++ b/lib/sdk/sdk_handler.go @@ -0,0 +1,66 @@ +package sdk + +import ( + "context" + "sync" + + mf "github.com/coder/agentapi/lib/cli/msgfmt" + "github.com/coder/agentapi/lib/types" + "github.com/danielgtaylor/huma/v2/sse" +) + +type SDKHandler struct { + agentType mf.AgentType + agentSDK SDK + conversation []types.ConversationMessage + mu sync.RWMutex +} + +func NewSDKHandler(ctx context.Context) *SDKHandler { + return &SDKHandler{} +} + +func (s *SDKHandler) GetStatus(ctx context.Context, input *struct{}) (*types.StatusResponse, error) { + return s.agentSDK.GetStatus() +} + +func (s *SDKHandler) GetMessages(ctx context.Context, input *struct{}) (*types.MessagesResponse, error) { + s.mu.RLock() + defer s.mu.RUnlock() + resp := &types.MessagesResponse{} + resp.Body.Messages = make([]types.Message, len(s.conversation)) + for i, msg := range s.conversation { + resp.Body.Messages[i] = types.Message{ + Id: msg.Id, + Role: msg.Role, + Content: msg.Message, + Time: msg.Time, + } + } + + return resp, nil +} + +// CreateMessage handles POST /message +func (s *SDKHandler) CreateMessage(ctx context.Context, input *types.MessageRequest) (*types.MessageResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + _, err := s.agentSDK.QueryAgent(input.Body.Content) + if err != nil { + return nil, err + } + resp := &types.MessageResponse{} + resp.Body.Ok = true + return resp, nil +} + +// SubscribeEvents is an SSE endpoint that sends events to the client +func (s *SDKHandler) SubscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) { + +} + +func (s *SDKHandler) SubscribeConversations(ctx context.Context, input *struct{}, send sse.Sender) { +} + +func (s *SDKHandler) StartSnapshotLoop(ctx context.Context) { +} diff --git a/lib/types/conversation.go b/lib/types/conversation.go new file mode 100644 index 0000000..5306556 --- /dev/null +++ b/lib/types/conversation.go @@ -0,0 +1,70 @@ +package types + +import ( + "time" + + "github.com/coder/agentapi/lib/util" + "github.com/danielgtaylor/huma/v2" +) + +type ConversationRole string + +const ( + ConversationRoleUser ConversationRole = "user" + ConversationRoleAgent ConversationRole = "agent" +) + +var ConversationRoleValues = []ConversationRole{ + ConversationRoleUser, + ConversationRoleAgent, +} + +type ConversationMessage struct { + Id int + Message string + Role ConversationRole + Time time.Time +} + +func (c ConversationRole) Schema(r huma.Registry) *huma.Schema { + return util.OpenAPISchema(r, "ConversationRole", ConversationRoleValues) +} + +type InteractionType string + +const ( + SDKInteractionType InteractionType = "sdk" + CLIInteractionType InteractionType = "cli" +) + +type MessageType string + +const ( + MessageTypeUser MessageType = "user" + MessageTypeRaw MessageType = "raw" +) + +var MessageTypeValues = []MessageType{ + MessageTypeUser, + MessageTypeRaw, +} + +func (m MessageType) Schema(r huma.Registry) *huma.Schema { + return util.OpenAPISchema(r, "MessageType", MessageTypeValues) +} + +type AgentStatus string + +const ( + AgentStatusRunning AgentStatus = "running" + AgentStatusStable AgentStatus = "stable" +) + +var AgentStatusValues = []AgentStatus{ + AgentStatusStable, + AgentStatusRunning, +} + +func (a AgentStatus) Schema(r huma.Registry) *huma.Schema { + return util.OpenAPISchema(r, "AgentStatus", AgentStatusValues) +} diff --git a/lib/httpapi/models.go b/lib/types/models.go similarity index 56% rename from lib/httpapi/models.go rename to lib/types/models.go index 8f969f9..bc03fa0 100644 --- a/lib/httpapi/models.go +++ b/lib/types/models.go @@ -1,35 +1,13 @@ -package httpapi +package types -import ( - "time" - - st "github.com/coder/agentapi/lib/screentracker" - "github.com/coder/agentapi/lib/util" - "github.com/danielgtaylor/huma/v2" -) - -type MessageType string - -const ( - MessageTypeUser MessageType = "user" - MessageTypeRaw MessageType = "raw" -) - -var MessageTypeValues = []MessageType{ - MessageTypeUser, - MessageTypeRaw, -} - -func (m MessageType) Schema(r huma.Registry) *huma.Schema { - return util.OpenAPISchema(r, "MessageType", MessageTypeValues) -} +import "time" // Message represents a message type Message struct { - Id int `json:"id" doc:"Unique identifier for the message. This identifier also represents the order of the message in the conversation history."` - Content string `json:"content" example:"Hello world" doc:"Message content. The message is formatted as it appears in the agent's terminal session, meaning that, by default, it consists of lines of text with 80 characters per line."` - Role st.ConversationRole `json:"role" doc:"Role of the message author"` - Time time.Time `json:"time" doc:"Timestamp of the message"` + Id int `json:"id" doc:"Unique identifier for the message. This identifier also represents the order of the message in the conversation history."` + Content string `json:"content" example:"Hello world" doc:"Message content. The message is formatted as it appears in the agent's terminal session, meaning that, by default, it consists of lines of text with 80 characters per line."` + Role ConversationRole `json:"role" doc:"Role of the message author"` + Time time.Time `json:"time" doc:"Timestamp of the message"` } // StatusResponse represents the server status @@ -62,3 +40,18 @@ type MessageResponse struct { Ok bool `json:"ok" doc:"Indicates whether the message was sent successfully. For messages of type 'user', success means detecting that the agent began executing the task described. For messages of type 'raw', success means the keystrokes were sent to the terminal."` } } + +type StatusChangeBody struct { + Status AgentStatus `json:"status" doc:"Agent status"` +} + +type ScreenUpdateBody struct { + Screen string `json:"screen"` +} + +type MessageUpdateBody struct { + Id int `json:"id" doc:"Unique identifier for the message. This identifier also represents the order of the message in the conversation history."` + Role ConversationRole `json:"role" doc:"Role of the message author"` + Message string `json:"message" doc:"Message content. The message is formatted as it appears in the agent's terminal session, meaning that, by default, it consists of lines of text with 80 characters per line."` + Time time.Time `json:"time" doc:"Timestamp of the message"` +} From 62caf2fb1f6afaf17a1bb320c02b8a158055f6f4 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 22 Sep 2025 22:24:06 +0530 Subject: [PATCH 2/9] chore: remove sdk parts --- lib/httpapi/handler.go | 8 ++-- lib/httpapi/server.go | 30 +++++++-------- lib/sdk/agents/claude/claude.go | 1 - lib/sdk/conversation.go | 9 ----- lib/sdk/sdk_handler.go | 66 --------------------------------- 5 files changed, 19 insertions(+), 95 deletions(-) delete mode 100644 lib/sdk/agents/claude/claude.go delete mode 100644 lib/sdk/conversation.go delete mode 100644 lib/sdk/sdk_handler.go diff --git a/lib/httpapi/handler.go b/lib/httpapi/handler.go index f162bd5..abbcdde 100644 --- a/lib/httpapi/handler.go +++ b/lib/httpapi/handler.go @@ -7,12 +7,12 @@ import ( "github.com/danielgtaylor/huma/v2/sse" ) -// InteractionHandler defines the interface that all interaction modes must implement -type InteractionHandler interface { +// AgentHandler defines the interface that all interaction modes must implement +type AgentHandler interface { GetStatus(ctx context.Context, input *struct{}) (*types.StatusResponse, error) CreateMessage(ctx context.Context, input *types.MessageRequest) (*types.MessageResponse, error) GetMessages(ctx context.Context, input *struct{}) (*types.MessagesResponse, error) - SubscribeConversations(ctx context.Context, input *struct{}, send sse.Sender) SubscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) - //StartSnapshotLoop(ctx context.Context) + // SubscribeConversations Was Initially SubscribeScreen, tbd whether we want to expose this in SDK mode TODO 1 + SubscribeConversations(ctx context.Context, input *struct{}, send sse.Sender) } diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index fea80ac..c5e67de 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -29,14 +29,14 @@ import ( // Server represents the HTTP server type Server struct { - router chi.Router - api huma.API - port int - srv *http.Server - logger *slog.Logger - agentType msgfmt.AgentType - chatBasePath string - InteractionHandler InteractionHandler + router chi.Router + api huma.API + port int + srv *http.Server + logger *slog.Logger + agentType msgfmt.AgentType + chatBasePath string + AgentHandler AgentHandler } func (s *Server) GetOpenAPI() string { @@ -202,9 +202,9 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { // Get the appropriate Interaction Handler if config.InteractionType == types.CLIInteractionType { - s.InteractionHandler = cli.NewCLIHandler(ctx, config.Process, config.AgentType) + s.AgentHandler = cli.NewCLIHandler(ctx, config.Process, config.AgentType) } else if config.InteractionType == types.SDKInteractionType { - s.InteractionHandler = sdk.NewSDKHandler(ctx) + s.AgentHandler = sdk.NewSDKHandler(ctx) } else { return nil, xerrors.Errorf("") } @@ -257,17 +257,17 @@ func hostAuthorizationMiddleware(allowedHosts []string, badHostHandler http.Hand // registerRoutes sets up all API endpoints func (s *Server) registerRoutes() { // GET /status endpoint - huma.Get(s.api, "/status", s.InteractionHandler.GetStatus, func(o *huma.Operation) { + huma.Get(s.api, "/status", s.AgentHandler.GetStatus, func(o *huma.Operation) { o.Description = "Returns the current status of the agent." }) // GET /messages endpoint - huma.Get(s.api, "/messages", s.InteractionHandler.GetMessages, func(o *huma.Operation) { + huma.Get(s.api, "/messages", s.AgentHandler.GetMessages, func(o *huma.Operation) { o.Description = "Returns a list of messages representing the conversation history with the agent." }) // POST /message endpoint - huma.Post(s.api, "/message", s.InteractionHandler.CreateMessage, func(o *huma.Operation) { + huma.Post(s.api, "/message", s.AgentHandler.CreateMessage, func(o *huma.Operation) { o.Description = "Send a message to the agent. For messages of type 'user', the agent's status must be 'stable' for the operation to complete successfully. Otherwise, this endpoint will return an error." }) @@ -282,7 +282,7 @@ func (s *Server) registerRoutes() { // Mapping of event type name to Go struct for that event. "message_update": types.MessageUpdateBody{}, "status_change": types.StatusChangeBody{}, - }, s.InteractionHandler.SubscribeEvents) + }, s.AgentHandler.SubscribeEvents) sse.Register(s.api, huma.Operation{ OperationID: "subscribeScreen", @@ -292,7 +292,7 @@ func (s *Server) registerRoutes() { Hidden: true, }, map[string]any{ "screen": types.ScreenUpdateBody{}, - }, s.InteractionHandler.SubscribeConversations) + }, s.AgentHandler.SubscribeConversations) s.router.Handle("/", http.HandlerFunc(s.redirectToChat)) diff --git a/lib/sdk/agents/claude/claude.go b/lib/sdk/agents/claude/claude.go deleted file mode 100644 index 6e25b57..0000000 --- a/lib/sdk/agents/claude/claude.go +++ /dev/null @@ -1 +0,0 @@ -package claude diff --git a/lib/sdk/conversation.go b/lib/sdk/conversation.go deleted file mode 100644 index 26a30ac..0000000 --- a/lib/sdk/conversation.go +++ /dev/null @@ -1,9 +0,0 @@ -package sdk - -import "github.com/coder/agentapi/lib/types" - -type SDK interface { - QueryAgent(userInput string) (string, error) - InitializeAgent(interface{}) error - GetStatus() (*types.StatusResponse, error) -} diff --git a/lib/sdk/sdk_handler.go b/lib/sdk/sdk_handler.go deleted file mode 100644 index eaace6b..0000000 --- a/lib/sdk/sdk_handler.go +++ /dev/null @@ -1,66 +0,0 @@ -package sdk - -import ( - "context" - "sync" - - mf "github.com/coder/agentapi/lib/cli/msgfmt" - "github.com/coder/agentapi/lib/types" - "github.com/danielgtaylor/huma/v2/sse" -) - -type SDKHandler struct { - agentType mf.AgentType - agentSDK SDK - conversation []types.ConversationMessage - mu sync.RWMutex -} - -func NewSDKHandler(ctx context.Context) *SDKHandler { - return &SDKHandler{} -} - -func (s *SDKHandler) GetStatus(ctx context.Context, input *struct{}) (*types.StatusResponse, error) { - return s.agentSDK.GetStatus() -} - -func (s *SDKHandler) GetMessages(ctx context.Context, input *struct{}) (*types.MessagesResponse, error) { - s.mu.RLock() - defer s.mu.RUnlock() - resp := &types.MessagesResponse{} - resp.Body.Messages = make([]types.Message, len(s.conversation)) - for i, msg := range s.conversation { - resp.Body.Messages[i] = types.Message{ - Id: msg.Id, - Role: msg.Role, - Content: msg.Message, - Time: msg.Time, - } - } - - return resp, nil -} - -// CreateMessage handles POST /message -func (s *SDKHandler) CreateMessage(ctx context.Context, input *types.MessageRequest) (*types.MessageResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - _, err := s.agentSDK.QueryAgent(input.Body.Content) - if err != nil { - return nil, err - } - resp := &types.MessageResponse{} - resp.Body.Ok = true - return resp, nil -} - -// SubscribeEvents is an SSE endpoint that sends events to the client -func (s *SDKHandler) SubscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) { - -} - -func (s *SDKHandler) SubscribeConversations(ctx context.Context, input *struct{}, send sse.Sender) { -} - -func (s *SDKHandler) StartSnapshotLoop(ctx context.Context) { -} From 45197995538dd306afc12f2ae3b6f7cd179200e3 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 22 Sep 2025 22:31:42 +0530 Subject: [PATCH 3/9] chore: remove sdk parts --- lib/httpapi/server.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index b58d43a..1dbc3ff 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -17,7 +17,6 @@ import ( "github.com/coder/agentapi/lib/cli/msgfmt" "github.com/coder/agentapi/lib/cli/termexec" "github.com/coder/agentapi/lib/logctx" - "github.com/coder/agentapi/lib/sdk" "github.com/coder/agentapi/lib/types" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" @@ -204,7 +203,7 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { if config.InteractionType == types.CLIInteractionType { s.AgentHandler = cli.NewCLIHandler(ctx, config.Process, config.AgentType) } else if config.InteractionType == types.SDKInteractionType { - s.AgentHandler = sdk.NewSDKHandler(ctx) + // TODO add a SDKHandler for SDK } else { return nil, xerrors.Errorf("") } From 510a7f09989ce44e91e6b142f6bb45ffea82a22c Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 22 Sep 2025 23:10:34 +0530 Subject: [PATCH 4/9] chore: update tests --- lib/cli/cli_handler.go | 5 +- lib/cli/events.go | 28 ++----- lib/cli/screentracker/conversation_test.go | 37 ++++----- lib/httpapi/handler.go | 2 + lib/httpapi/server.go | 6 +- lib/httpapi/server_test.go | 93 ++++++++++++---------- 6 files changed, 84 insertions(+), 87 deletions(-) diff --git a/lib/cli/cli_handler.go b/lib/cli/cli_handler.go index f500aa8..f59e10c 100644 --- a/lib/cli/cli_handler.go +++ b/lib/cli/cli_handler.go @@ -39,7 +39,7 @@ func convertStatus(status st.ConversationStatus) types.AgentStatus { } } -func NewCLIHandler(ctx context.Context, agentio *termexec.Process, agentType mf.AgentType) *CLIHandler { +func NewCLIHandler(ctx context.Context, logger *slog.Logger, agentio *termexec.Process, agentType mf.AgentType) *CLIHandler { formatMessage := func(message string, userInput string) string { return mf.FormatAgentMessage(agentType, message, userInput) } @@ -61,10 +61,9 @@ func NewCLIHandler(ctx context.Context, agentio *termexec.Process, agentType mf. conversation: conversation, agentio: agentio, agentType: agentType, + logger: logger, } - handler.StartSnapshotLoop(ctx) - return handler } diff --git a/lib/cli/events.go b/lib/cli/events.go index 669f0a0..2c2f51d 100644 --- a/lib/cli/events.go +++ b/lib/cli/events.go @@ -3,7 +3,6 @@ package cli import ( "strings" "sync" - "time" mf "github.com/coder/agentapi/lib/cli/msgfmt" st "github.com/coder/agentapi/lib/cli/screentracker" @@ -18,21 +17,6 @@ const ( EventTypeScreenUpdate EventType = "screen_update" ) -type MessageUpdateBody struct { - Id int `json:"id" doc:"Unique identifier for the message. This identifier also represents the order of the message in the conversation history."` - Role types.ConversationRole `json:"role" doc:"Role of the message author"` - Message string `json:"message" doc:"Message content. The message is formatted as it appears in the agent's terminal session, meaning that, by default, it consists of lines of text with 80 characters per line."` - Time time.Time `json:"time" doc:"Timestamp of the message"` -} - -type StatusChangeBody struct { - Status types.AgentStatus `json:"status" doc:"Agent status"` -} - -type ScreenUpdateBody struct { - Screen string `json:"screen"` -} - type Event struct { Type EventType Payload any @@ -104,7 +88,7 @@ func (e *EventEmitter) UpdateMessagesAndEmitChanges(newMessages []types.Conversa newMsg = newMessages[i] } if oldMsg != newMsg { - e.notifyChannels(EventTypeMessageUpdate, MessageUpdateBody{ + e.notifyChannels(EventTypeMessageUpdate, types.MessageUpdateBody{ Id: newMessages[i].Id, Role: newMessages[i].Role, Message: newMessages[i].Message, @@ -125,7 +109,7 @@ func (e *EventEmitter) UpdateStatusAndEmitChanges(newStatus st.ConversationStatu return } - e.notifyChannels(EventTypeStatusChange, StatusChangeBody{Status: newAgentStatus}) + e.notifyChannels(EventTypeStatusChange, types.StatusChangeBody{Status: newAgentStatus}) e.status = newAgentStatus } @@ -137,7 +121,7 @@ func (e *EventEmitter) UpdateScreenAndEmitChanges(newScreen string) { return } - e.notifyChannels(EventTypeScreenUpdate, ScreenUpdateBody{Screen: strings.TrimRight(newScreen, mf.WhiteSpaceChars)}) + e.notifyChannels(EventTypeScreenUpdate, types.ScreenUpdateBody{Screen: strings.TrimRight(newScreen, mf.WhiteSpaceChars)}) e.screen = newScreen } @@ -147,16 +131,16 @@ func (e *EventEmitter) currentStateAsEvents() []Event { for _, msg := range e.messages { events = append(events, Event{ Type: EventTypeMessageUpdate, - Payload: MessageUpdateBody{Id: msg.Id, Role: msg.Role, Message: msg.Message, Time: msg.Time}, + Payload: types.MessageUpdateBody{Id: msg.Id, Role: msg.Role, Message: msg.Message, Time: msg.Time}, }) } events = append(events, Event{ Type: EventTypeStatusChange, - Payload: StatusChangeBody{Status: e.status}, + Payload: types.StatusChangeBody{Status: e.status}, }) events = append(events, Event{ Type: EventTypeScreenUpdate, - Payload: ScreenUpdateBody{Screen: strings.TrimRight(e.screen, mf.WhiteSpaceChars)}, + Payload: types.ScreenUpdateBody{Screen: strings.TrimRight(e.screen, mf.WhiteSpaceChars)}, }) return events } diff --git a/lib/cli/screentracker/conversation_test.go b/lib/cli/screentracker/conversation_test.go index 1561400..2f59e09 100644 --- a/lib/cli/screentracker/conversation_test.go +++ b/lib/cli/screentracker/conversation_test.go @@ -10,6 +10,7 @@ import ( "github.com/coder/agentapi/lib/cli/msgfmt" st "github.com/coder/agentapi/lib/cli/screentracker" + "github.com/coder/agentapi/lib/types" "github.com/stretchr/testify/assert" ) @@ -116,16 +117,16 @@ func TestConversation(t *testing.T) { func TestMessages(t *testing.T) { now := time.Now() - agentMsg := func(id int, msg string) st.ConversationMessage { - return st.ConversationMessage{ + agentMsg := func(id int, msg string) types.ConversationMessage { + return types.ConversationMessage{ Id: id, Message: msg, Role: st.ConversationRoleAgent, Time: now, } } - userMsg := func(id int, msg string) st.ConversationMessage { - return st.ConversationMessage{ + userMsg := func(id int, msg string) types.ConversationMessage { + return types.ConversationMessage{ Id: id, Message: msg, Role: st.ConversationRoleUser, @@ -152,13 +153,13 @@ func TestMessages(t *testing.T) { t.Run("messages are copied", func(t *testing.T) { c := newConversation() messages := c.Messages() - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, ""), }, messages) messages[0].Message = "modification" - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, ""), }, c.Messages()) }) @@ -183,7 +184,7 @@ func TestMessages(t *testing.T) { c.AddSnapshot("1") msgs := c.Messages() - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "1"), }, msgs) nowWrapper.Time = nowWrapper.Add(1 * time.Second) @@ -198,27 +199,27 @@ func TestMessages(t *testing.T) { }) // agent message is recorded when the first snapshot is added c.AddSnapshot("1") - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "1"), }, c.Messages()) // agent message is updated when the screen changes c.AddSnapshot("2") - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "2"), }, c.Messages()) // user message is recorded agent.screen = "2" assert.NoError(t, sendMsg(c, "3")) - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "2"), userMsg(1, "3"), }, c.Messages()) // agent message is added after a user message c.AddSnapshot("4") - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "2"), userMsg(1, "3"), agentMsg(2, "4"), @@ -227,7 +228,7 @@ func TestMessages(t *testing.T) { // agent message is updated when the screen changes before a user message agent.screen = "5" assert.NoError(t, sendMsg(c, "6")) - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "2"), userMsg(1, "3"), agentMsg(2, "5"), @@ -241,7 +242,7 @@ func TestMessages(t *testing.T) { assert.Equal(t, st.ConversationStatusStable, c.Status()) agent.screen = "7" assert.NoError(t, sendMsg(c, "8")) - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "2"), userMsg(1, "3"), agentMsg(2, "5"), @@ -268,7 +269,7 @@ func TestMessages(t *testing.T) { agent.screen = "1" assert.NoError(t, sendMsg(c, "2")) c.AddSnapshot("1\n3") - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "1"), userMsg(1, "2"), agentMsg(2, "3"), @@ -277,7 +278,7 @@ func TestMessages(t *testing.T) { agent.screen = "1\n3x" assert.NoError(t, sendMsg(c, "4")) c.AddSnapshot("1\n3x\n5") - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "1"), userMsg(1, "2"), agentMsg(2, "3x"), @@ -296,13 +297,13 @@ func TestMessages(t *testing.T) { }) agent.screen = "1" assert.NoError(t, sendMsg(c, "2")) - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "1 "), userMsg(1, "2"), }, c.Messages()) agent.screen = "x" c.AddSnapshot("x") - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ agentMsg(0, "1 "), userMsg(1, "2"), agentMsg(2, "x 2"), @@ -317,7 +318,7 @@ func TestMessages(t *testing.T) { return "formatted" } }) - assert.Equal(t, []st.ConversationMessage{ + assert.Equal(t, []types.ConversationMessage{ { Id: 0, Message: "", diff --git a/lib/httpapi/handler.go b/lib/httpapi/handler.go index abbcdde..8d4d390 100644 --- a/lib/httpapi/handler.go +++ b/lib/httpapi/handler.go @@ -13,6 +13,8 @@ type AgentHandler interface { CreateMessage(ctx context.Context, input *types.MessageRequest) (*types.MessageResponse, error) GetMessages(ctx context.Context, input *struct{}) (*types.MessagesResponse, error) SubscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) + // SubscribeConversations Was Initially SubscribeScreen, tbd whether we want to expose this in SDK mode TODO 1 SubscribeConversations(ctx context.Context, input *struct{}, send sse.Sender) + StartSnapshotLoop(ctx context.Context) } diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index 1dbc3ff..a628659 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -201,16 +201,18 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { // Get the appropriate Interaction Handler if config.InteractionType == types.CLIInteractionType { - s.AgentHandler = cli.NewCLIHandler(ctx, config.Process, config.AgentType) + s.AgentHandler = cli.NewCLIHandler(ctx, logger, config.Process, config.AgentType) } else if config.InteractionType == types.SDKInteractionType { // TODO add a SDKHandler for SDK } else { - return nil, xerrors.Errorf("") + return nil, xerrors.Errorf("%s", config.InteractionType) } // Register API routes s.registerRoutes() + s.AgentHandler.StartSnapshotLoop(ctx) + return s, nil } diff --git a/lib/httpapi/server_test.go b/lib/httpapi/server_test.go index fceaa52..286480c 100644 --- a/lib/httpapi/server_test.go +++ b/lib/httpapi/server_test.go @@ -15,6 +15,8 @@ import ( "github.com/coder/agentapi/lib/cli/msgfmt" "github.com/coder/agentapi/lib/httpapi" "github.com/coder/agentapi/lib/logctx" + "github.com/coder/agentapi/lib/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -47,12 +49,13 @@ func TestOpenAPISchema(t *testing.T) { ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil))) srv, err := httpapi.NewServer(ctx, httpapi.ServerConfig{ - AgentType: msgfmt.AgentTypeClaude, - Process: nil, - Port: 0, - ChatBasePath: "/chat", - AllowedHosts: []string{"*"}, - AllowedOrigins: []string{"*"}, + AgentType: msgfmt.AgentTypeClaude, + Process: nil, + Port: 0, + ChatBasePath: "/chat", + AllowedHosts: []string{"*"}, + AllowedOrigins: []string{"*"}, + InteractionType: types.CLIInteractionType, }) require.NoError(t, err) currentSchemaStr := srv.GetOpenAPI() @@ -99,12 +102,13 @@ func TestServer_redirectToChat(t *testing.T) { t.Parallel() tCtx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil))) s, err := httpapi.NewServer(tCtx, httpapi.ServerConfig{ - AgentType: msgfmt.AgentTypeClaude, - Process: nil, - Port: 0, - ChatBasePath: tc.chatBasePath, - AllowedHosts: []string{"*"}, - AllowedOrigins: []string{"*"}, + AgentType: msgfmt.AgentTypeClaude, + Process: nil, + Port: 0, + ChatBasePath: tc.chatBasePath, + AllowedHosts: []string{"*"}, + AllowedOrigins: []string{"*"}, + InteractionType: types.CLIInteractionType, }) require.NoError(t, err) tsServer := httptest.NewServer(s.Handler()) @@ -263,12 +267,13 @@ func TestServer_AllowedHosts(t *testing.T) { t.Parallel() ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil))) s, err := httpapi.NewServer(ctx, httpapi.ServerConfig{ - AgentType: msgfmt.AgentTypeClaude, - Process: nil, - Port: 0, - ChatBasePath: "/chat", - AllowedHosts: tc.allowedHosts, - AllowedOrigins: []string{"https://example.com"}, // Set a default to isolate host testing + AgentType: msgfmt.AgentTypeClaude, + Process: nil, + Port: 0, + ChatBasePath: "/chat", + AllowedHosts: tc.allowedHosts, + AllowedOrigins: []string{"https://example.com"}, // Set a default to isolate host testing + InteractionType: types.CLIInteractionType, }) if tc.validationErrorMsg != "" { require.Error(t, err) @@ -346,12 +351,13 @@ func TestServer_CORSPreflightWithHosts(t *testing.T) { t.Parallel() ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil))) s, err := httpapi.NewServer(ctx, httpapi.ServerConfig{ - AgentType: msgfmt.AgentTypeClaude, - Process: nil, - Port: 0, - ChatBasePath: "/chat", - AllowedHosts: tc.allowedHosts, - AllowedOrigins: []string{"*"}, // Set wildcard origins to isolate host testing + AgentType: msgfmt.AgentTypeClaude, + Process: nil, + Port: 0, + ChatBasePath: "/chat", + AllowedHosts: tc.allowedHosts, + AllowedOrigins: []string{"*"}, // Set wildcard origins to isolate host testing + InteractionType: types.CLIInteractionType, }) require.NoError(t, err) tsServer := httptest.NewServer(s.Handler()) @@ -505,12 +511,13 @@ func TestServer_CORSOrigins(t *testing.T) { t.Parallel() ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil))) s, err := httpapi.NewServer(ctx, httpapi.ServerConfig{ - AgentType: msgfmt.AgentTypeClaude, - Process: nil, - Port: 0, - ChatBasePath: "/chat", - AllowedHosts: []string{"*"}, // Set wildcard to isolate CORS testing - AllowedOrigins: tc.allowedOrigins, + AgentType: msgfmt.AgentTypeClaude, + Process: nil, + Port: 0, + ChatBasePath: "/chat", + AllowedHosts: []string{"*"}, // Set wildcard to isolate CORS testing + AllowedOrigins: tc.allowedOrigins, + InteractionType: types.CLIInteractionType, }) if tc.validationErrorMsg != "" { require.Error(t, err) @@ -585,12 +592,13 @@ func TestServer_CORSPreflightOrigins(t *testing.T) { t.Parallel() ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil))) s, err := httpapi.NewServer(ctx, httpapi.ServerConfig{ - AgentType: msgfmt.AgentTypeClaude, - Process: nil, - Port: 0, - ChatBasePath: "/chat", - AllowedHosts: []string{"*"}, // Set wildcard to isolate CORS testing - AllowedOrigins: tc.allowedOrigins, + AgentType: msgfmt.AgentTypeClaude, + Process: nil, + Port: 0, + ChatBasePath: "/chat", + AllowedHosts: []string{"*"}, // Set wildcard to isolate CORS testing + AllowedOrigins: tc.allowedOrigins, + InteractionType: types.CLIInteractionType, }) require.NoError(t, err) tsServer := httptest.NewServer(s.Handler()) @@ -636,12 +644,13 @@ func TestServer_SSEMiddleware_Events(t *testing.T) { t.Parallel() ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil))) srv, err := httpapi.NewServer(ctx, httpapi.ServerConfig{ - AgentType: msgfmt.AgentTypeClaude, - Process: nil, - Port: 0, - ChatBasePath: "/chat", - AllowedHosts: []string{"*"}, - AllowedOrigins: []string{"*"}, + AgentType: msgfmt.AgentTypeClaude, + Process: nil, + Port: 0, + ChatBasePath: "/chat", + AllowedHosts: []string{"*"}, + AllowedOrigins: []string{"*"}, + InteractionType: types.CLIInteractionType, }) require.NoError(t, err) tsServer := httptest.NewServer(srv.Handler()) From 3a539312af23bd1a44e3e984e76b71007d34ce43 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 22 Sep 2025 23:17:14 +0530 Subject: [PATCH 5/9] chore: lint fix --- lib/httpapi/server.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index a628659..c63e24b 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -9,7 +9,6 @@ import ( "net/url" "slices" "strings" - "time" "unicode" "github.com/coder/agentapi/internal/version" @@ -55,10 +54,6 @@ func (s *Server) GetOpenAPI() string { return string(prettyJSON) } -// That's about 40 frames per second. It's slightly less -// because the action of taking a snapshot takes time too. -const snapshotInterval = 25 * time.Millisecond - type ServerConfig struct { AgentType msgfmt.AgentType InteractionType types.InteractionType From a56cfe7b5badb825d3955d6455493467cd3ef4fe Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 23 Sep 2025 20:16:28 +0530 Subject: [PATCH 6/9] feat: uncomment test --- lib/cli/events_test.go | 199 ++++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 100 deletions(-) diff --git a/lib/cli/events_test.go b/lib/cli/events_test.go index 99417f0..abe46e8 100644 --- a/lib/cli/events_test.go +++ b/lib/cli/events_test.go @@ -1,102 +1,101 @@ package cli -// -//import ( -// "fmt" -// "testing" -// "time" -// -// st "github.com/coder/agentapi/lib/cli/screentracker" -// "github.com/coder/agentapi/lib/httpapi" -// "github.com/stretchr/testify/assert" -//) -// -//func TestEventEmitter(t *testing.T) { -// t.Run("single-subscription", func(t *testing.T) { -// emitter := httpapi.NewEventEmitter(10) -// _, ch, stateEvents := emitter.Subscribe() -// assert.Empty(t, ch) -// assert.Equal(t, []httpapi.Event{ -// { -// Type: httpapi.EventTypeStatusChange, -// Payload: httpapi.StatusChangeBody{Status: httpapi.AgentStatusRunning}, -// }, -// { -// Type: httpapi.EventTypeScreenUpdate, -// Payload: httpapi.ConversationUpdateBody{Screen: ""}, -// }, -// }, stateEvents) -// -// now := time.Now() -// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ -// {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, -// }) -// newEvent := <-ch -// assert.Equal(t, httpapi.Event{ -// Type: httpapi.EventTypeMessageUpdate, -// Payload: httpapi.MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, -// }, newEvent) -// -// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ -// {Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, -// {Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, -// }) -// newEvent = <-ch -// assert.Equal(t, httpapi.Event{ -// Type: httpapi.EventTypeMessageUpdate, -// Payload: httpapi.MessageUpdateBody{Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, -// }, newEvent) -// -// newEvent = <-ch -// assert.Equal(t, httpapi.Event{ -// Type: httpapi.EventTypeMessageUpdate, -// Payload: httpapi.MessageUpdateBody{Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, -// }, newEvent) -// -// emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable) -// newEvent = <-ch -// assert.Equal(t, httpapi.Event{ -// Type: httpapi.EventTypeStatusChange, -// Payload: httpapi.StatusChangeBody{Status: httpapi.AgentStatusStable}, -// }, newEvent) -// }) -// -// t.Run("multiple-subscriptions", func(t *testing.T) { -// emitter := httpapi.NewEventEmitter(10) -// channels := make([]<-chan httpapi.Event, 0, 10) -// for i := 0; i < 10; i++ { -// _, ch, _ := emitter.Subscribe() -// channels = append(channels, ch) -// } -// now := time.Now() -// -// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ -// {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, -// }) -// for _, ch := range channels { -// newEvent := <-ch -// assert.Equal(t, httpapi.Event{ -// Type: httpapi.EventTypeMessageUpdate, -// Payload: httpapi.MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, -// }, newEvent) -// } -// }) -// -// t.Run("close-channel", func(t *testing.T) { -// emitter := httpapi.NewEventEmitter(1) -// _, ch, _ := emitter.Subscribe() -// for i := range 5 { -// emitter.UpdateMessagesAndEmitChanges([]st.ConversationMessage{ -// {Id: i, Message: fmt.Sprintf("Hello, world! %d", i), Role: st.ConversationRoleUser, Time: time.Now()}, -// }) -// } -// _, ok := <-ch -// assert.True(t, ok) -// select { -// case _, ok := <-ch: -// assert.False(t, ok) -// default: -// t.Fatalf("read should not block") -// } -// }) -//} +import ( + "fmt" + "testing" + "time" + + st "github.com/coder/agentapi/lib/cli/screentracker" + "github.com/coder/agentapi/lib/types" + "github.com/stretchr/testify/assert" +) + +func TestEventEmitter(t *testing.T) { + t.Run("single-subscription", func(t *testing.T) { + emitter := NewEventEmitter(10) + _, ch, stateEvents := emitter.Subscribe() + assert.Empty(t, ch) + assert.Equal(t, []Event{ + { + Type: EventTypeStatusChange, + Payload: types.StatusChangeBody{Status: types.AgentStatusRunning}, + }, + { + Type: EventTypeScreenUpdate, + Payload: types.ScreenUpdateBody{Screen: ""}, + }, + }, stateEvents) + + now := time.Now() + emitter.UpdateMessagesAndEmitChanges([]types.ConversationMessage{ + {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, + }) + newEvent := <-ch + assert.Equal(t, Event{ + Type: EventTypeMessageUpdate, + Payload: types.MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, + }, newEvent) + + emitter.UpdateMessagesAndEmitChanges([]types.ConversationMessage{ + {Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, + {Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, + }) + newEvent = <-ch + assert.Equal(t, Event{ + Type: EventTypeMessageUpdate, + Payload: types.MessageUpdateBody{Id: 1, Message: "Hello, world! (updated)", Role: st.ConversationRoleUser, Time: now}, + }, newEvent) + + newEvent = <-ch + assert.Equal(t, Event{ + Type: EventTypeMessageUpdate, + Payload: types.MessageUpdateBody{Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, + }, newEvent) + + emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable) + newEvent = <-ch + assert.Equal(t, Event{ + Type: EventTypeStatusChange, + Payload: types.StatusChangeBody{Status: types.AgentStatusStable}, + }, newEvent) + }) + + t.Run("multiple-subscriptions", func(t *testing.T) { + emitter := NewEventEmitter(10) + channels := make([]<-chan Event, 0, 10) + for i := 0; i < 10; i++ { + _, ch, _ := emitter.Subscribe() + channels = append(channels, ch) + } + now := time.Now() + + emitter.UpdateMessagesAndEmitChanges([]types.ConversationMessage{ + {Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, + }) + for _, ch := range channels { + newEvent := <-ch + assert.Equal(t, Event{ + Type: EventTypeMessageUpdate, + Payload: types.MessageUpdateBody{Id: 1, Message: "Hello, world!", Role: st.ConversationRoleUser, Time: now}, + }, newEvent) + } + }) + + t.Run("close-channel", func(t *testing.T) { + emitter := NewEventEmitter(1) + _, ch, _ := emitter.Subscribe() + for i := range 5 { + emitter.UpdateMessagesAndEmitChanges([]types.ConversationMessage{ + {Id: i, Message: fmt.Sprintf("Hello, world! %d", i), Role: st.ConversationRoleUser, Time: time.Now()}, + }) + } + _, ok := <-ch + assert.True(t, ok) + select { + case _, ok := <-ch: + assert.False(t, ok) + default: + t.Fatalf("read should not block") + } + }) +} From a8a899ca90361db6e40f0b25a03a97b324e59712 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 23 Sep 2025 21:06:20 +0530 Subject: [PATCH 7/9] chore: refactor InteractionType and error --- cmd/server/server.go | 2 +- lib/httpapi/server.go | 6 +++--- lib/httpapi/server_test.go | 14 +++++++------- lib/types/conversation.go | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index c951112..89f07c2 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -70,7 +70,7 @@ func parseAgentType(firstArg string, agentTypeVar string) (AgentType, error) { } func parseInteractionType(interactionModeVar string) (types.InteractionType, error) { - return types.CLIInteractionType, nil + return types.InteractionTypeCLI, nil } func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) error { diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index c63e24b..a9b7979 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -195,12 +195,12 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { } // Get the appropriate Interaction Handler - if config.InteractionType == types.CLIInteractionType { + if config.InteractionType == types.InteractionTypeCLI { s.AgentHandler = cli.NewCLIHandler(ctx, logger, config.Process, config.AgentType) - } else if config.InteractionType == types.SDKInteractionType { + } else if config.InteractionType == types.InteractionTypeSDK { // TODO add a SDKHandler for SDK } else { - return nil, xerrors.Errorf("%s", config.InteractionType) + return nil, xerrors.Errorf("unknown interaction type %q", config.InteractionType) } // Register API routes diff --git a/lib/httpapi/server_test.go b/lib/httpapi/server_test.go index 286480c..2000efc 100644 --- a/lib/httpapi/server_test.go +++ b/lib/httpapi/server_test.go @@ -55,7 +55,7 @@ func TestOpenAPISchema(t *testing.T) { ChatBasePath: "/chat", AllowedHosts: []string{"*"}, AllowedOrigins: []string{"*"}, - InteractionType: types.CLIInteractionType, + InteractionType: types.InteractionTypeCLI, }) require.NoError(t, err) currentSchemaStr := srv.GetOpenAPI() @@ -108,7 +108,7 @@ func TestServer_redirectToChat(t *testing.T) { ChatBasePath: tc.chatBasePath, AllowedHosts: []string{"*"}, AllowedOrigins: []string{"*"}, - InteractionType: types.CLIInteractionType, + InteractionType: types.InteractionTypeCLI, }) require.NoError(t, err) tsServer := httptest.NewServer(s.Handler()) @@ -273,7 +273,7 @@ func TestServer_AllowedHosts(t *testing.T) { ChatBasePath: "/chat", AllowedHosts: tc.allowedHosts, AllowedOrigins: []string{"https://example.com"}, // Set a default to isolate host testing - InteractionType: types.CLIInteractionType, + InteractionType: types.InteractionTypeCLI, }) if tc.validationErrorMsg != "" { require.Error(t, err) @@ -357,7 +357,7 @@ func TestServer_CORSPreflightWithHosts(t *testing.T) { ChatBasePath: "/chat", AllowedHosts: tc.allowedHosts, AllowedOrigins: []string{"*"}, // Set wildcard origins to isolate host testing - InteractionType: types.CLIInteractionType, + InteractionType: types.InteractionTypeCLI, }) require.NoError(t, err) tsServer := httptest.NewServer(s.Handler()) @@ -517,7 +517,7 @@ func TestServer_CORSOrigins(t *testing.T) { ChatBasePath: "/chat", AllowedHosts: []string{"*"}, // Set wildcard to isolate CORS testing AllowedOrigins: tc.allowedOrigins, - InteractionType: types.CLIInteractionType, + InteractionType: types.InteractionTypeCLI, }) if tc.validationErrorMsg != "" { require.Error(t, err) @@ -598,7 +598,7 @@ func TestServer_CORSPreflightOrigins(t *testing.T) { ChatBasePath: "/chat", AllowedHosts: []string{"*"}, // Set wildcard to isolate CORS testing AllowedOrigins: tc.allowedOrigins, - InteractionType: types.CLIInteractionType, + InteractionType: types.InteractionTypeCLI, }) require.NoError(t, err) tsServer := httptest.NewServer(s.Handler()) @@ -650,7 +650,7 @@ func TestServer_SSEMiddleware_Events(t *testing.T) { ChatBasePath: "/chat", AllowedHosts: []string{"*"}, AllowedOrigins: []string{"*"}, - InteractionType: types.CLIInteractionType, + InteractionType: types.InteractionTypeCLI, }) require.NoError(t, err) tsServer := httptest.NewServer(srv.Handler()) diff --git a/lib/types/conversation.go b/lib/types/conversation.go index 5306556..a131712 100644 --- a/lib/types/conversation.go +++ b/lib/types/conversation.go @@ -33,8 +33,8 @@ func (c ConversationRole) Schema(r huma.Registry) *huma.Schema { type InteractionType string const ( - SDKInteractionType InteractionType = "sdk" - CLIInteractionType InteractionType = "cli" + InteractionTypeSDK InteractionType = "sdk" + InteractionTypeCLI InteractionType = "cli" ) type MessageType string From cf3e045604fd0eaaf5522db1cb8af093b119966a Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 1 Oct 2025 22:53:39 +0530 Subject: [PATCH 8/9] chore: fix conflicts --- .../msgfmt/testdata/format/copilot/confirmation_box/expected.txt | 0 .../msgfmt/testdata/format/copilot/confirmation_box/msg.txt | 0 .../msgfmt/testdata/format/copilot/confirmation_box/user.txt | 0 .../msgfmt/testdata/format/copilot/first_message/expected.txt | 0 .../msgfmt/testdata/format/copilot/first_message/msg.txt | 0 .../msgfmt/testdata/format/copilot/first_message/user.txt | 0 .../msgfmt/testdata/format/copilot/multi-line-input/expected.txt | 0 .../msgfmt/testdata/format/copilot/multi-line-input/msg.txt | 0 .../msgfmt/testdata/format/copilot/multi-line-input/user.txt | 0 .../msgfmt/testdata/format/copilot/thinking/expected.txt | 0 lib/{ => cli}/msgfmt/testdata/format/copilot/thinking/msg.txt | 0 lib/{ => cli}/msgfmt/testdata/format/copilot/thinking/user.txt | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename lib/{ => cli}/msgfmt/testdata/format/copilot/confirmation_box/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/confirmation_box/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/confirmation_box/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/first_message/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/first_message/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/first_message/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/multi-line-input/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/multi-line-input/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/multi-line-input/user.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/thinking/expected.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/thinking/msg.txt (100%) rename lib/{ => cli}/msgfmt/testdata/format/copilot/thinking/user.txt (100%) diff --git a/lib/msgfmt/testdata/format/copilot/confirmation_box/expected.txt b/lib/cli/msgfmt/testdata/format/copilot/confirmation_box/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/confirmation_box/expected.txt rename to lib/cli/msgfmt/testdata/format/copilot/confirmation_box/expected.txt diff --git a/lib/msgfmt/testdata/format/copilot/confirmation_box/msg.txt b/lib/cli/msgfmt/testdata/format/copilot/confirmation_box/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/confirmation_box/msg.txt rename to lib/cli/msgfmt/testdata/format/copilot/confirmation_box/msg.txt diff --git a/lib/msgfmt/testdata/format/copilot/confirmation_box/user.txt b/lib/cli/msgfmt/testdata/format/copilot/confirmation_box/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/confirmation_box/user.txt rename to lib/cli/msgfmt/testdata/format/copilot/confirmation_box/user.txt diff --git a/lib/msgfmt/testdata/format/copilot/first_message/expected.txt b/lib/cli/msgfmt/testdata/format/copilot/first_message/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/first_message/expected.txt rename to lib/cli/msgfmt/testdata/format/copilot/first_message/expected.txt diff --git a/lib/msgfmt/testdata/format/copilot/first_message/msg.txt b/lib/cli/msgfmt/testdata/format/copilot/first_message/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/first_message/msg.txt rename to lib/cli/msgfmt/testdata/format/copilot/first_message/msg.txt diff --git a/lib/msgfmt/testdata/format/copilot/first_message/user.txt b/lib/cli/msgfmt/testdata/format/copilot/first_message/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/first_message/user.txt rename to lib/cli/msgfmt/testdata/format/copilot/first_message/user.txt diff --git a/lib/msgfmt/testdata/format/copilot/multi-line-input/expected.txt b/lib/cli/msgfmt/testdata/format/copilot/multi-line-input/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/multi-line-input/expected.txt rename to lib/cli/msgfmt/testdata/format/copilot/multi-line-input/expected.txt diff --git a/lib/msgfmt/testdata/format/copilot/multi-line-input/msg.txt b/lib/cli/msgfmt/testdata/format/copilot/multi-line-input/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/multi-line-input/msg.txt rename to lib/cli/msgfmt/testdata/format/copilot/multi-line-input/msg.txt diff --git a/lib/msgfmt/testdata/format/copilot/multi-line-input/user.txt b/lib/cli/msgfmt/testdata/format/copilot/multi-line-input/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/multi-line-input/user.txt rename to lib/cli/msgfmt/testdata/format/copilot/multi-line-input/user.txt diff --git a/lib/msgfmt/testdata/format/copilot/thinking/expected.txt b/lib/cli/msgfmt/testdata/format/copilot/thinking/expected.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/thinking/expected.txt rename to lib/cli/msgfmt/testdata/format/copilot/thinking/expected.txt diff --git a/lib/msgfmt/testdata/format/copilot/thinking/msg.txt b/lib/cli/msgfmt/testdata/format/copilot/thinking/msg.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/thinking/msg.txt rename to lib/cli/msgfmt/testdata/format/copilot/thinking/msg.txt diff --git a/lib/msgfmt/testdata/format/copilot/thinking/user.txt b/lib/cli/msgfmt/testdata/format/copilot/thinking/user.txt similarity index 100% rename from lib/msgfmt/testdata/format/copilot/thinking/user.txt rename to lib/cli/msgfmt/testdata/format/copilot/thinking/user.txt From b4afe2a040d24bc37c03caef50d3d895d589e89e Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 1 Oct 2025 23:29:49 +0530 Subject: [PATCH 9/9] chore: fix agentapi attach --- cmd/attach/attach.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/attach/attach.go b/cmd/attach/attach.go index 522f2f4..54a69ad 100644 --- a/cmd/attach/attach.go +++ b/cmd/attach/attach.go @@ -73,6 +73,7 @@ func (m model) View() string { } func ReadScreenOverHTTP(ctx context.Context, url string, ch chan<- types.ScreenUpdateBody) error { + fmt.Println(url) req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req.Header.Set("Content-Type", "application/json") @@ -150,7 +151,7 @@ func runAttach(remoteUrl string) error { readScreenErrCh := make(chan error, 1) go func() { defer close(readScreenErrCh) - if err := ReadScreenOverHTTP(ctx, remoteUrl+"/internal/conversation", screenCh); err != nil { + if err := ReadScreenOverHTTP(ctx, remoteUrl+"/internal/screen", screenCh); err != nil { if errors.Is(err, context.Canceled) { return } @@ -227,6 +228,7 @@ var AttachCmd = &cobra.Command{ Long: `Attach to a running agent`, Run: func(cmd *cobra.Command, args []string) { remoteUrl := remoteUrlArg + fmt.Println(remoteUrl) if remoteUrl == "" { fmt.Fprintln(os.Stderr, "URL is required") os.Exit(1)