diff --git a/.env b/.env new file mode 100644 index 0000000..95d0ab1 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +AGENTAPI_ALLOWED_HOSTS="dev.otherstuff.studio dev.otherstuff.ai localhost" diff --git a/AGENTS.md b/AGENTS.md index d0397a3..44cb280 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ This file provides guidance to AI agents working with code in this repository. - `agentapi server -- goose` - Start server with Goose agent - `agentapi server --type=codex -- codex` - Start server with Codex (requires explicit type) - `agentapi server --type=gemini -- gemini` - Start server with Gemini (requires explicit type) +- `agentapi server --tmux-session=my-app -- claude` - Override the tmux session name (defaults to `wingman-agents`) - `agentapi attach --url localhost:3284` - Attach to running agent terminal - Server runs on port 3284 by default - Chat UI available at http://localhost:3284/chat diff --git a/chat/src/components/message-input.tsx b/chat/src/components/message-input.tsx index 0748dee..edd9705 100644 --- a/chat/src/components/message-input.tsx +++ b/chat/src/components/message-input.tsx @@ -7,6 +7,7 @@ import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, + Command as CommandIcon, CornerDownLeftIcon, DeleteIcon, SendIcon, @@ -14,6 +15,12 @@ import { Square, } from "lucide-react"; import {Tabs, TabsList, TabsTrigger} from "./ui/tabs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; import type {ServerStatus} from "./chat-provider"; import TextareaAutosize from "react-textarea-autosize"; import {useChat} from "./chat-provider"; @@ -50,6 +57,32 @@ const specialKeys: Record = { Backspace: "\b", // Backspace key }; +const SHIFT_TAB_SEQUENCE = "\x1b[Z"; +const CLEAR_SCREEN_SEQUENCE = "\x0C"; + +interface ControlAction { + id: string; + label: string; + display: string; + sequence: string; +} + +const CONTROL_ACTIONS: ControlAction[] = [ + {id: "escape", label: "Esc", display: "Esc", sequence: specialKeys.Escape}, + {id: "enter", label: "Enter", display: "⏎", sequence: "\r"}, + {id: "tab", label: "Tab", display: "Tab", sequence: specialKeys.Tab}, + {id: "shift-tab", label: "Shift+Tab", display: "Shift+Tab", sequence: SHIFT_TAB_SEQUENCE}, + {id: "one-enter", label: "1 + Enter", display: "1⏎", sequence: "1\r"}, + {id: "two-enter", label: "2 + Enter", display: "2⏎", sequence: "2\r"}, + {id: "three-enter", label: "3 + Enter", display: "3⏎", sequence: "3\r"}, + {id: "ctrl-c", label: "Ctrl+C", display: "Ctrl+C", sequence: "\x03"}, + {id: "arrow-up", label: "Up", display: "ArrowUp", sequence: specialKeys.ArrowUp}, + {id: "arrow-down", label: "Down", display: "ArrowDown", sequence: specialKeys.ArrowDown}, + {id: "arrow-left", label: "Left", display: "ArrowLeft", sequence: specialKeys.ArrowLeft}, + {id: "arrow-right", label: "Right", display: "ArrowRight", sequence: specialKeys.ArrowRight}, + {id: "clear", label: "Clear", display: "Clear", sequence: CLEAR_SCREEN_SEQUENCE}, +]; + export default function MessageInput({ onSendMessage, disabled = false, @@ -121,9 +154,25 @@ export default function MessageInput({ setSentChars((prev) => [...prev, newChar]); }; + const handleControlAction = (action: ControlAction) => { + if (disabled || inputMode !== "control") { + return; + } + addSentChar(action.display); + onSendMessage(action.sequence, "raw"); + textareaRef.current?.focus(); + }; + const handleKeyDown = (e: KeyboardEvent) => { // In control mode, send special keys as raw messages if (inputMode === "control" && !disabled) { + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + addSentChar("Shift+Tab"); + onSendMessage(SHIFT_TAB_SEQUENCE, "raw"); + return; + } + // Check if the pressed key is in our special keys map if (specialKeys[e.key]) { e.preventDefault(); @@ -241,26 +290,54 @@ export default function MessageInput({
- - { - textareaRef.current?.focus(); - }} - > - Text - - { - textareaRef.current?.focus(); - }} - > - Control - - +
+ + { + textareaRef.current?.focus(); + }} + > + Text + + { + textareaRef.current?.focus(); + }} + > + Control + + + + {inputMode === "control" && !disabled && ( + + + + + + {CONTROL_ACTIONS.map((action) => ( + handleControlAction(action)} + > + {action.label} + + ))} + + + )} +
-
+