From 1b85035829cdfe2101f458cb93d505d2c6e65e77 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 18 Mar 2026 07:19:27 +0000 Subject: [PATCH] feat: add Agent/Plan/Ask mode system with tab cycling and OpenCode home screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AgentMode type (agent/plan/ask) with per-mode system prompts - Agent mode: full tool access, executes changes directly - Plan mode: read-only tools, creates implementation plans without modifying files - Ask mode: read-only tools, answers questions with context - Tab key cycles between modes (visible in prompt box) - OpenCode-style block-letter GROK logo on home screen (left=muted, right=bold) - Prompt box shows mode label + model name + provider (like OpenCode's prompt) - Shadow/bottom-edge effect below prompt (╹ + ▀ pattern from OpenCode) - Keyboard hints bar: 'tab modes · ctrl+p commands' (matches OpenCode layout) - Mode color changes left-border accent on prompt and user messages - Refactored into HomeView and SessionView components Co-authored-by: Ismail Pelaseyed --- src/agent/agent.ts | 98 +++++--- src/types/index.ts | 8 + src/ui/app.tsx | 544 ++++++++++++++++++++------------------------- 3 files changed, 319 insertions(+), 331 deletions(-) diff --git a/src/agent/agent.ts b/src/agent/agent.ts index 4f41209..6483cf2 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -2,42 +2,65 @@ import { GrokClient } from "../grok/client.js"; import { TOOLS } from "../grok/tools.js"; import { BashTool } from "../tools/bash.js"; import { loadCustomInstructions } from "../utils/instructions.js"; -import type { ChatEntry, Message, StreamChunk, ToolCall, ToolResult } from "../types/index.js"; +import type { AgentMode, ChatEntry, Message, StreamChunk, ToolCall, ToolResult } from "../types/index.js"; const MAX_TOOL_ROUNDS = 400; -function buildSystemPrompt(cwd: string): string { - const custom = loadCustomInstructions(); - const customSection = custom - ? `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above custom instructions alongside the standard instructions.\n` - : ""; - - return `You are Grok CLI, a powerful AI coding agent running in the terminal. You operate directly through bash commands.${customSection} +const MODE_PROMPTS: Record = { + agent: `You are Grok CLI in Agent mode — a powerful AI coding agent. You execute tasks directly using tools. TOOLS: -- bash: Execute any shell command. Use this for EVERYTHING — viewing files (cat, bat, less), editing files (sed, awk, tee, or write with heredocs), searching code (grep, rg, find), git operations, running builds, installing packages, etc. -- search_web: Search the internet for real-time information, documentation, APIs, current events. -- search_x: Search X (Twitter) for posts, discussions, trends, community sentiment. +- bash: Execute any shell command — viewing files, editing (sed/tee/heredocs), searching (grep/rg/find), git, builds, packages. +- search_web: Real-time web search for documentation, APIs, current events. +- search_x: Search X (Twitter) for posts, discussions, trends. WORKFLOW: -1. Understand what the user wants -2. Use bash to explore, read files, and understand the codebase -3. Make changes using bash commands (sed, tee, heredocs, etc.) -4. Verify your changes by reading the modified files -5. Run any relevant tests or builds to confirm correctness +1. Understand the request +2. Use bash to explore and understand the codebase +3. Make changes using bash commands +4. Verify changes by reading modified files +5. Run tests or builds to confirm correctness -FILE EDITING PATTERNS: -- For small edits: sed -i 's/old/new/g' file -- For creating files: cat > file << 'EOF' ... EOF -- For appending: cat >> file << 'EOF' ... EOF -- For complex edits: Use a combination of sed, awk, or write the entire file with cat/tee -- Always verify edits by reading the file after modification +Be direct. Execute, don't just describe. Show results, not plans.`, -RESPONSE STYLE: -- Be direct and concise -- Show what you're doing, not what you plan to do -- After tool use, give brief confirmation or note issues -- Use markdown formatting in responses + plan: `You are Grok CLI in Plan mode — you analyze and plan but DO NOT execute changes. + +TOOLS: +- bash: ONLY use for reading files (cat, head, tail, find, ls, grep) — NEVER modify files. +- search_web: Research documentation and current information. +- search_x: Research discussions and community sentiment. + +BEHAVIOR: +- Create a detailed, step-by-step implementation plan +- Identify files that need changes and describe the specific edits +- Highlight potential risks, edge cases, and dependencies +- Suggest a testing strategy +- NEVER create, modify, or delete files — only read and analyze + +Format your plan with clear numbered steps and file paths.`, + + ask: `You are Grok CLI in Ask mode — you answer questions clearly and thoroughly. + +TOOLS: +- bash: ONLY for reading files to provide context (cat, find, ls, grep) — NEVER modify. +- search_web: Look up documentation and current information. +- search_x: Research discussions and trends. + +BEHAVIOR: +- Answer the user's question directly and thoroughly +- Use tools to gather context when needed +- Provide code examples when helpful +- NEVER create, modify, or delete files +- Focus on explanation, not execution`, +}; + +function buildSystemPrompt(cwd: string, mode: AgentMode): string { + const custom = loadCustomInstructions(); + const customSection = custom + ? `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above alongside standard instructions.\n` + : ""; + + return `${MODE_PROMPTS[mode]}${customSection} Current working directory: ${cwd}`; } @@ -49,6 +72,7 @@ export class Agent { private history: ChatEntry[] = []; private abortController: AbortController | null = null; private maxToolRounds: number; + private mode: AgentMode = "agent"; constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number) { this.client = new GrokClient(apiKey, model, baseURL); @@ -56,7 +80,7 @@ export class Agent { this.maxToolRounds = maxToolRounds || MAX_TOOL_ROUNDS; this.messages.push({ role: "system", - content: buildSystemPrompt(process.cwd()), + content: buildSystemPrompt(process.cwd(), this.mode), }); } @@ -68,6 +92,18 @@ export class Agent { this.client.setModel(model); } + getMode(): AgentMode { + return this.mode; + } + + setMode(mode: AgentMode): void { + this.mode = mode; + this.messages[0] = { + role: "system", + content: buildSystemPrompt(this.bash.getCwd(), mode), + }; + } + getHistory(): ChatEntry[] { return [...this.history]; } @@ -81,8 +117,10 @@ export class Agent { } clearHistory(): void { - const systemMsg = this.messages[0]; - this.messages = [systemMsg]; + this.messages = [{ + role: "system", + content: buildSystemPrompt(this.bash.getCwd(), this.mode), + }]; this.history = []; } diff --git a/src/types/index.ts b/src/types/index.ts index 97ac281..95d1159 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,3 +60,11 @@ export interface ModelInfo { reasoning: boolean; description: string; } + +export type AgentMode = "agent" | "plan" | "ask"; + +export const MODES: { id: AgentMode; label: string; color: string }[] = [ + { id: "agent", label: "Agent", color: "#fab283" }, + { id: "plan", label: "Plan", color: "#9d7cd8" }, + { id: "ask", label: "Ask", color: "#5c9cf5" }, +]; diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 2aacd19..9e65d41 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -2,25 +2,26 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useKeyboard, useTerminalDimensions } from "@opentui/react"; import type { InputRenderable, ScrollBoxRenderable } from "@opentui/core"; import type { Agent } from "../agent/agent.js"; -import type { ChatEntry, ToolCall } from "../types/index.js"; +import type { ChatEntry, ToolCall, AgentMode } from "../types/index.js"; +import { MODES } from "../types/index.js"; import { getModelInfo } from "../grok/models.js"; import { getAvailableModels, saveProjectSettings } from "../utils/settings.js"; import { dark, type Theme } from "./theme.js"; const SPLIT_BORDER = { - topLeft: "", - bottomLeft: "", - vertical: "┃", - topRight: "", - bottomRight: "", - horizontal: " ", - bottomT: "", - topT: "", - cross: "", - leftT: "", - rightT: "", + topLeft: "", bottomLeft: "", vertical: "┃", topRight: "", + bottomRight: "", horizontal: " ", bottomT: "", topT: "", + cross: "", leftT: "", rightT: "", }; +const SPLIT_BORDER_END = { + ...SPLIT_BORDER, + bottomLeft: "╹", +}; + +const LOGO_LEFT = [" ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀▄", "█ __ █__█ █ █ █__█", "▀▀▀▀ █ ▀▀▀▀ ▀ ▀"]; +const LOGO_RIGHT = [" ", "█▀▀▀ █ █", "█ █ █", "▀▀▀▀ ▀▀▀▀"]; + interface AppProps { agent: Agent; initialMessage?: string; @@ -33,9 +34,9 @@ export function App({ agent, initialMessage }: AppProps) { const [streamReasoning, setStreamReasoning] = useState(""); const [isProcessing, setIsProcessing] = useState(false); const [model, setModel] = useState(agent.getModel()); + const [mode, setModeState] = useState(agent.getMode()); const [showModelPicker, setShowModelPicker] = useState(false); const [modelPickerIndex, setModelPickerIndex] = useState(0); - const [showHelp, setShowHelp] = useState(false); const [activeToolCalls, setActiveToolCalls] = useState([]); const inputRef = useRef(null); const scrollRef = useRef(null); @@ -43,28 +44,34 @@ export function App({ agent, initialMessage }: AppProps) { const processedInitial = useRef(false); const contentAccRef = useRef(""); + const setMode = useCallback((m: AgentMode) => { + agent.setMode(m); + setModeState(m); + }, [agent]); + + const cycleMode = useCallback(() => { + const idx = MODES.findIndex((m) => m.id === mode); + const next = MODES[(idx + 1) % MODES.length]; + setMode(next.id); + }, [mode, setMode]); + + const modeInfo = MODES.find((m) => m.id === mode)!; + const modelInfo = getModelInfo(model); + const scrollToBottom = useCallback(() => { - try { - scrollRef.current?.scrollTo(scrollRef.current?.scrollHeight ?? 99999); - } catch { - /* ignore */ - } + try { scrollRef.current?.scrollTo(scrollRef.current?.scrollHeight ?? 99999); } catch { /* */ } }, []); const processMessage = useCallback( async (text: string) => { if (!text.trim() || isProcessing) return; - setIsProcessing(true); setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); contentAccRef.current = ""; - setMessages((prev) => [ - ...prev, - { type: "user", content: text.trim(), timestamp: new Date() }, - ]); + setMessages((prev) => [...prev, { type: "user", content: text.trim(), timestamp: new Date() }]); setTimeout(scrollToBottom, 50); try { @@ -82,10 +89,7 @@ export function App({ agent, initialMessage }: AppProps) { if (chunk.toolCalls) { if (contentAccRef.current.trim()) { const saved = contentAccRef.current; - setMessages((prev) => [ - ...prev, - { type: "assistant", content: saved, timestamp: new Date() }, - ]); + setMessages((prev) => [...prev, { type: "assistant", content: saved, timestamp: new Date() }]); contentAccRef.current = ""; setStreamContent(""); } @@ -94,18 +98,11 @@ export function App({ agent, initialMessage }: AppProps) { break; case "tool_result": if (chunk.toolCall && chunk.toolResult) { - setMessages((prev) => [ - ...prev, - { - type: "tool_result", - content: chunk.toolResult!.success - ? chunk.toolResult!.output || "Success" - : chunk.toolResult!.error || "Error", - timestamp: new Date(), - toolCall: chunk.toolCall, - toolResult: chunk.toolResult, - }, - ]); + setMessages((prev) => [...prev, { + type: "tool_result", + content: chunk.toolResult!.success ? (chunk.toolResult!.output || "Success") : (chunk.toolResult!.error || "Error"), + timestamp: new Date(), toolCall: chunk.toolCall, toolResult: chunk.toolResult, + }]); setActiveToolCalls([]); setTimeout(scrollToBottom, 10); } @@ -114,8 +111,7 @@ export function App({ agent, initialMessage }: AppProps) { contentAccRef.current += `\n${chunk.content || "Unknown error"}`; setStreamContent(contentAccRef.current); break; - case "done": - break; + case "done": break; } } } catch { @@ -125,10 +121,7 @@ export function App({ agent, initialMessage }: AppProps) { if (contentAccRef.current.trim()) { const final = contentAccRef.current; - setMessages((prev) => [ - ...prev, - { type: "assistant", content: final, timestamp: new Date() }, - ]); + setMessages((prev) => [...prev, { type: "assistant", content: final, timestamp: new Date() }]); } contentAccRef.current = ""; @@ -151,7 +144,7 @@ export function App({ agent, initialMessage }: AppProps) { const handleCommand = useCallback( (cmd: string): boolean => { const c = cmd.trim().toLowerCase(); - if (c === "/help") { setShowHelp((v) => !v); return true; } + if (c === "/help") { return true; } if (c === "/clear") { agent.clearHistory(); setMessages([]); setStreamContent(""); return true; } if (c === "/model" || c === "/models") { setShowModelPicker(true); setModelPickerIndex(0); return true; } if (c === "/quit" || c === "/exit" || c === "/q") { process.exit(0); } @@ -174,8 +167,8 @@ export function App({ agent, initialMessage }: AppProps) { } return; } - if (showHelp && key.name === "escape") { setShowHelp(false); return; } if (isProcessing && key.name === "escape") { agent.abort(); return; } + if (key.name === "tab" && !isProcessing) { cycleMode(); return; } }); const handleSubmit = useCallback(() => { @@ -194,161 +187,224 @@ export function App({ agent, initialMessage }: AppProps) { return ( - {/* Main content area */} - - {hasMessages ? ( - <> -
- - {showHelp && } - {messages.map((msg, i) => ( - - ))} - {activeToolCalls.length > 0 && activeToolCalls.map((tc, i) => ( - - ))} - {streamReasoning && ( - - - {"Thinking: "}{truncate(streamReasoning, 300)} - - - )} - {streamContent && ( - - {streamContent} - - )} - {isProcessing && !streamContent && activeToolCalls.length === 0 && ( - - {"~ Thinking..."} - - )} - - - + {hasMessages ? ( + + ) : ( + + )} + + ); +} + +/* ── Home Screen ─────────────────────────────────────────────────── */ + +function HomeView({ t, agent, model, mode, modeInfo, modelInfo, inputRef, isProcessing, showModelPicker, handleSubmit }: { + t: Theme; agent: Agent; model: string; mode: AgentMode; + modeInfo: typeof MODES[number]; modelInfo: ReturnType; + inputRef: React.RefObject; + isProcessing: boolean; showModelPicker: boolean; handleSubmit: () => void; +}) { + return ( + <> + + + + {/* Logo */} + + {LOGO_LEFT.map((line, i) => ( + + {line} + {LOGO_RIGHT[i]} - + ))} + + + {/* Prompt box */} + + + + {/* Keyboard hints */} + + + {"tab "}{"modes"} + {"ctrl+p "}{"commands"} + + + + + {/* Footer */} + + {agent.getCwd()} + + {"v1.0.0"} + + + ); +} + +/* ── Session View ────────────────────────────────────────────────── */ + +function SessionView({ t, agent, model, mode, modeInfo, modelInfo, messages, streamContent, streamReasoning, isProcessing, activeToolCalls, scrollRef, inputRef, showModelPicker, handleSubmit, scrollToBottom }: { + t: Theme; agent: Agent; model: string; mode: AgentMode; + modeInfo: typeof MODES[number]; modelInfo: ReturnType; + messages: ChatEntry[]; streamContent: string; streamReasoning: string; + isProcessing: boolean; activeToolCalls: ToolCall[]; + scrollRef: React.RefObject; + inputRef: React.RefObject; + showModelPicker: boolean; handleSubmit: () => void; scrollToBottom: () => void; +}) { + return ( + +
+ + {messages.map((msg, i) => ( + + ))} + {activeToolCalls.length > 0 && activeToolCalls.map((tc, i) => ( + + ))} + {streamReasoning && ( + + {"Thinking: "}{truncate(streamReasoning, 300)} + + )} + {streamContent && ( + + {streamContent} + + )} + {isProcessing && !streamContent && activeToolCalls.length === 0 && ( + + {"~ Thinking..."} + + )} + + + + +