From 60035f2240cefbb48189d46e863d2d5827a29d78 Mon Sep 17 00:00:00 2001 From: Ismail Pelaseyed Date: Wed, 18 Mar 2026 15:29:00 +0100 Subject: [PATCH] feat: redesign model picker and slash menu modals - Remove borders, add search filtering with keyboard input - Flat model list without category sections - Blue accent color for current model highlight - Fit-to-content modals with max height cap and scroll - scrollChildIntoView for arrow key navigation - Trim logo height by removing decorative lines - Add accent color to theme Made-with: Cursor --- .cursor/.gitignore | 1 + bun.lock | 6 +- package.json | 1 - src/agent/agent.ts | 33 +--- src/grok/client.ts | 34 +++- src/grok/models.ts | 8 +- src/index.ts | 6 +- src/types/index.ts | 7 +- src/ui/app.tsx | 404 +++++++++++++++++++++++++++++++----------- src/ui/theme.ts | 1 + src/utils/settings.ts | 7 +- 11 files changed, 354 insertions(+), 154 deletions(-) create mode 100644 .cursor/.gitignore diff --git a/.cursor/.gitignore b/.cursor/.gitignore new file mode 100644 index 0000000..8bf7cc2 --- /dev/null +++ b/.cursor/.gitignore @@ -0,0 +1 @@ +plans/ diff --git a/bun.lock b/bun.lock index 54f33c8..5532dd7 100644 --- a/bun.lock +++ b/bun.lock @@ -1,13 +1,11 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "@vibe-kit/grok-cli", "dependencies": { "@opentui/core": "^0.1.88", "@opentui/react": "^0.1.88", - "chalk": "^5.3.0", "commander": "^12.0.0", "dotenv": "^16.4.0", "openai": "^5.10.1", @@ -204,7 +202,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -464,8 +462,6 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/package.json b/package.json index 3862186..7dc0491 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "dependencies": { "@opentui/core": "^0.1.88", "@opentui/react": "^0.1.88", - "chalk": "^5.3.0", "commander": "^12.0.0", "dotenv": "^16.4.0", "openai": "^5.10.1", diff --git a/src/agent/agent.ts b/src/agent/agent.ts index 6483cf2..d69ccb0 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -2,7 +2,7 @@ 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 { AgentMode, ChatEntry, Message, StreamChunk, ToolCall, ToolResult } from "../types/index.js"; +import type { AgentMode, Message, StreamChunk, ToolCall, ToolResult } from "../types/index.js"; const MAX_TOOL_ROUNDS = 400; @@ -69,7 +69,6 @@ export class Agent { private client: GrokClient; private bash: BashTool; private messages: Message[] = []; - private history: ChatEntry[] = []; private abortController: AbortController | null = null; private maxToolRounds: number; private mode: AgentMode = "agent"; @@ -104,14 +103,14 @@ export class Agent { }; } - getHistory(): ChatEntry[] { - return [...this.history]; - } - getCwd(): string { return this.bash.getCwd(); } + generateTitle(userMessage: string): Promise { + return this.client.generateTitle(userMessage); + } + abort(): void { this.abortController?.abort(); } @@ -121,18 +120,11 @@ export class Agent { role: "system", content: buildSystemPrompt(this.bash.getCwd(), this.mode), }]; - this.history = []; } async *processMessage(userMessage: string): AsyncGenerator { this.abortController = new AbortController(); - const userEntry: ChatEntry = { - type: "user", - content: userMessage, - timestamp: new Date(), - }; - this.history.push(userEntry); this.messages.push({ role: "user", content: userMessage }); let toolRounds = 0; @@ -178,13 +170,6 @@ export class Agent { tool_calls: accToolCalls, }); - this.history.push({ - type: "assistant", - content: accContent || "", - timestamp: new Date(), - toolCalls: accToolCalls, - }); - if (!accToolCalls || accToolCalls.length === 0) { break; } @@ -202,14 +187,6 @@ export class Agent { yield { type: "tool_result", toolCall: tc, toolResult: result }; - this.history.push({ - type: "tool_result", - content: result.success ? (result.output || "Success") : (result.error || "Error"), - timestamp: new Date(), - toolCall: tc, - toolResult: result, - }); - this.messages.push({ role: "tool", content: result.success ? (result.output || "Success") : (result.error || "Error"), diff --git a/src/grok/client.ts b/src/grok/client.ts index 91ddc32..ea82de3 100644 --- a/src/grok/client.ts +++ b/src/grok/client.ts @@ -6,7 +6,6 @@ export interface StreamDelta { content?: string; toolCalls?: ToolCall[]; reasoning?: string; - finishReason?: string | null; } export class GrokClient { @@ -60,7 +59,7 @@ export class GrokClient { const choice = chunk.choices?.[0]; if (!choice) continue; - const delta: StreamDelta = { finishReason: choice.finish_reason }; + const delta: StreamDelta = {}; if (choice.delta?.content) { delta.content = choice.delta.content; @@ -105,6 +104,37 @@ export class GrokClient { return this.search(query, "x_search"); } + async generateTitle(userMessage: string): Promise { + try { + const response = await this.client.chat.completions.create({ + model: "grok-3-mini-fast", + temperature: 0.5, + max_tokens: 60, + messages: [ + { + role: "system", + content: [ + "You are a title generator. Output ONLY a short title. Nothing else.", + "Rules:", + "- Single line, ≤50 characters", + "- Use the same language as the user message", + "- Focus on the main topic or intent", + "- Keep technical terms, filenames, numbers exact", + "- Remove filler words (the, this, my, a, an)", + "- Never use tools or explain anything", + "- If the message is a greeting, output something like 'Quick chat'", + ].join("\n"), + }, + { role: "user", content: userMessage }, + ], + }); + const title = response.choices?.[0]?.message?.content?.trim().replace(/^["']|["']$/g, ""); + return title || "New session"; + } catch { + return "New session"; + } + } + private async search(query: string, toolType: "web_search" | "x_search"): Promise { try { const response = await this.client.responses.create({ diff --git a/src/grok/models.ts b/src/grok/models.ts index ceeee2b..4fcd4f5 100644 --- a/src/grok/models.ts +++ b/src/grok/models.ts @@ -89,10 +89,4 @@ export function getModelInfo(modelId: string): ModelInfo | undefined { export function getModelIds(): string[] { return MODELS.map((m) => m.id); -} - -export function formatPrice(price: number): string { - if (price === 0) return "Free"; - if (price < 1) return `$${price.toFixed(2)}`; - return `$${price}`; -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 0841db8..5c34c33 100755 --- a/src/index.ts +++ b/src/index.ts @@ -35,10 +35,12 @@ async function startInteractive( const agent = new Agent(apiKey, baseURL, model, maxToolRounds); const renderer = await createCliRenderer({ - exitOnCtrlC: true, + exitOnCtrlC: false, }); - createRoot(renderer).render(createElement(App, { agent, initialMessage })); + const onExit = () => { renderer.destroy(); process.exit(0); }; + + createRoot(renderer).render(createElement(App, { agent, initialMessage, onExit })); } async function runHeadless( diff --git a/src/types/index.ts b/src/types/index.ts index 7f42d61..bfd4ac3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -34,17 +34,16 @@ export interface ToolDefinition { } export interface ChatEntry { - type: "user" | "assistant" | "tool_call" | "tool_result" | "search_result"; + type: "user" | "assistant" | "tool_call" | "tool_result"; content: string; timestamp: Date; toolCalls?: ToolCall[]; toolCall?: ToolCall; toolResult?: ToolResult; - isStreaming?: boolean; } export interface StreamChunk { - type: "content" | "tool_calls" | "tool_result" | "search_result" | "done" | "error" | "reasoning"; + type: "content" | "tool_calls" | "tool_result" | "done" | "error" | "reasoning"; content?: string; toolCalls?: ToolCall[]; toolCall?: ToolCall; @@ -66,5 +65,5 @@ export type AgentMode = "agent" | "plan" | "ask"; export const MODES: { id: AgentMode; label: string; color: string }[] = [ { id: "agent", label: "Agent", color: "#5c9cf5" }, { id: "plan", label: "Plan", color: "#e5c07b" }, - { id: "ask", label: "Ask", color: "#f5a742" }, + { id: "ask", label: "Ask", color: "#22c55e" }, ]; diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 29365a6..594aab1 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -1,13 +1,24 @@ +import os from "os"; import { useState, useEffect, useCallback, useRef } from "react"; import { useKeyboard, useTerminalDimensions } from "@opentui/react"; -import type { InputRenderable, ScrollBoxRenderable } from "@opentui/core"; +import type { ScrollBoxRenderable, TextareaRenderable, KeyBinding } from "@opentui/core"; +import { decodePasteBytes, PasteEvent } from "@opentui/core"; import type { Agent } from "../agent/agent.js"; -import type { ChatEntry, ToolCall, AgentMode } from "../types/index.js"; +import type { ChatEntry, ToolCall, AgentMode, ModelInfo } from "../types/index.js"; import { MODES } from "../types/index.js"; -import { getModelInfo, MODEL_GROUPS, MODELS } from "../grok/models.js"; +import { getModelInfo, MODELS } from "../grok/models.js"; import { saveProjectSettings } from "../utils/settings.js"; import { dark, type Theme } from "./theme.js"; +const GROK_LOGO = [ + " █▃▆▃▐▃▆▃=. <▆█", + " ▅▆< .@▆# <▆█", + "@▆░ I▀▅▌▌▌ @▆▐▌▌▃▆! <▆█ ~▆▀", + "▒▆░ .*▆▆▆▆▆ _▆▁ ▒▆= i▅▄I<▆█|▁▂/", + "+▆▐: I▁▆ _▆▁ ▆▆= ▅▂\\<▆▓|▃▄l", + " █▆▃l....▆▆! _▆▁ ,▆▀i .█▆▌ <▆█ ▃▆/", +].join("\n"); + const SPLIT = { topLeft: "", bottomLeft: "", vertical: "┃", topRight: "", bottomRight: "", horizontal: " ", bottomT: "", topT: "", @@ -20,9 +31,25 @@ const EMPTY = { cross: "", leftT: "", rightT: "", }; -interface AppProps { agent: Agent; initialMessage?: string } +interface SlashMenuItem { + id: string; + label: string; + description: string; +} -export function App({ agent, initialMessage }: AppProps) { +const SLASH_MENU_ITEMS: SlashMenuItem[] = [ + { id: "exit", label: "exit", description: "Quit the CLI" }, + { id: "help", label: "help", description: "Show available commands" }, + { id: "mcps", label: "mcps", description: "Manage MCP servers" }, + { id: "models", label: "models", description: "Select a model" }, + { id: "new", label: "new session", description: "Start a new session" }, + { id: "review", label: "review", description: "Review recent changes" }, + { id: "skills", label: "skills", description: "Manage skills" }, +]; + +interface AppProps { agent: Agent; initialMessage?: string; onExit?: () => void } + +export function App({ agent, initialMessage, onExit }: AppProps) { const t = dark; const [messages, setMessages] = useState([]); const [streamContent, setStreamContent] = useState(""); @@ -32,9 +59,16 @@ export function App({ agent, initialMessage }: AppProps) { const [mode, setModeState] = useState(agent.getMode()); const [showModelPicker, setShowModelPicker] = useState(false); const [modelPickerIndex, setModelPickerIndex] = useState(0); + const [modelSearchQuery, setModelSearchQuery] = useState(""); const [activeToolCalls, setActiveToolCalls] = useState([]); - const [toolCallCount, setToolCallCount] = useState(0); - const inputRef = useRef(null); + const [sessionTitle, setSessionTitle] = useState(null); + const [showSlashMenu, setShowSlashMenu] = useState(false); + const [slashMenuIndex, setSlashMenuIndex] = useState(0); + const [slashSearchQuery, setSlashSearchQuery] = useState(""); + const [pasteBlocks, setPasteBlocks] = useState<{ id: number; content: string; lines: number; isImage?: boolean }[]>([]); + const imageCounterRef = useRef(0); + const pasteCounterRef = useRef(0); + const inputRef = useRef(null); const scrollRef = useRef(null); const { width, height } = useTerminalDimensions(); const processedInitial = useRef(false); @@ -50,6 +84,19 @@ export function App({ agent, initialMessage }: AppProps) { const modeInfo = MODES.find((m) => m.id === mode)!; const modelInfo = getModelInfo(model); const flatModels = MODELS.map((m) => m.id); + const filteredModels = modelSearchQuery + ? MODELS.filter((m) => + m.name.toLowerCase().includes(modelSearchQuery.toLowerCase()) || + m.id.toLowerCase().includes(modelSearchQuery.toLowerCase()) + ) + : MODELS; + const filteredModelIds = filteredModels.map((m) => m.id); + const filteredSlashItems = slashSearchQuery + ? SLASH_MENU_ITEMS.filter((item) => + item.label.toLowerCase().includes(slashSearchQuery.toLowerCase()) || + item.description.toLowerCase().includes(slashSearchQuery.toLowerCase()) + ) + : SLASH_MENU_ITEMS; const scrollToBottom = useCallback(() => { try { scrollRef.current?.scrollTo(scrollRef.current?.scrollHeight ?? 99999); } catch { /* */ } @@ -58,7 +105,8 @@ export function App({ agent, initialMessage }: AppProps) { const processMessage = useCallback(async (text: string) => { if (!text.trim() || isProcessing) return; setIsProcessing(true); setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); contentAccRef.current = ""; - setToolCallCount(0); startTimeRef.current = Date.now(); + startTimeRef.current = Date.now(); + if (!sessionTitle) agent.generateTitle(text.trim()).then(setSessionTitle).catch(() => {}); setMessages((prev) => [...prev, { type: "user", content: text.trim(), timestamp: new Date() }]); setTimeout(scrollToBottom, 50); try { @@ -80,7 +128,6 @@ export function App({ agent, initialMessage }: AppProps) { contentAccRef.current = ""; setStreamContent(""); } setActiveToolCalls(chunk.toolCalls); - setToolCallCount((c) => c + chunk.toolCalls!.length); } break; case "tool_result": @@ -119,35 +166,137 @@ export function App({ agent, initialMessage }: AppProps) { const handleCommand = useCallback((cmd: string): boolean => { const c = cmd.trim().toLowerCase(); - if (c === "/clear") { agent.clearHistory(); setMessages([]); setStreamContent(""); return true; } - if (c === "/model" || c === "/models") { setShowModelPicker(true); setModelPickerIndex(Math.max(0, flatModels.indexOf(model))); return true; } - if (c === "/quit" || c === "/exit" || c === "/q") { process.exit(0); } + if (c === "/clear") { agent.clearHistory(); setMessages([]); setStreamContent(""); setSessionTitle(null); setPasteBlocks([]); imageCounterRef.current = 0; return true; } + if (c === "/model" || c === "/models") { setShowModelPicker(true); setModelPickerIndex(0); setModelSearchQuery(""); return true; } + if (c === "/quit" || c === "/exit" || c === "/q") { onExit ? onExit() : process.exit(0); } return false; }, [agent, model, flatModels]); + const handleSlashMenuSelect = useCallback((item: SlashMenuItem) => { + setShowSlashMenu(false); + inputRef.current?.clear(); + switch (item.id) { + case "new": + agent.clearHistory(); setMessages([]); setStreamContent(""); setSessionTitle(null); setPasteBlocks([]); + break; + case "models": + setShowModelPicker(true); setModelPickerIndex(0); setModelSearchQuery(""); + break; + case "exit": + onExit ? onExit() : process.exit(0); + break; + case "help": + setMessages((p) => [...p, { type: "assistant", content: SLASH_MENU_ITEMS.map((i) => `/${i.label} — ${i.description}`).join("\n"), timestamp: new Date() }]); + break; + case "skills": + setMessages((p) => [...p, { type: "assistant", content: "Skills management coming soon.", timestamp: new Date() }]); + break; + case "mcps": + setMessages((p) => [...p, { type: "assistant", content: "MCP server management coming soon.", timestamp: new Date() }]); + break; + case "review": + setMessages((p) => [...p, { type: "assistant", content: "Review feature coming soon.", timestamp: new Date() }]); + break; + } + }, [agent, flatModels, model]); + useKeyboard((key) => { - if (showModelPicker) { - if (key.name === "escape") { setShowModelPicker(false); return; } - if (key.name === "up") { setModelPickerIndex((i) => Math.max(0, i - 1)); return; } - if (key.name === "down") { setModelPickerIndex((i) => Math.min(flatModels.length - 1, i + 1)); return; } + if (showSlashMenu) { + if (key.name === "escape") { setShowSlashMenu(false); setSlashSearchQuery(""); inputRef.current?.clear(); return; } + if (key.name === "up") { setSlashMenuIndex((i) => Math.max(0, i - 1)); return; } + if (key.name === "down") { setSlashMenuIndex((i) => Math.min(filteredSlashItems.length - 1, i + 1)); return; } if (key.name === "return") { - const sel = flatModels[modelPickerIndex]; + const item = filteredSlashItems[slashMenuIndex]; + if (item) handleSlashMenuSelect(item); + setSlashSearchQuery(""); + return; + } + if (key.name === "backspace") { + setSlashSearchQuery((q) => q.slice(0, -1)); + setSlashMenuIndex(0); + return; + } + if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) { + setSlashSearchQuery((q) => q + key.sequence); + setSlashMenuIndex(0); + return; + } + return; + } + if (showModelPicker) { + if (key.name === "escape") { setShowModelPicker(false); setModelSearchQuery(""); return; } + if (key.name === "up") { setModelPickerIndex((i) => Math.max(0, i - 1)); return; } + if (key.name === "down") { setModelPickerIndex((i) => Math.min(filteredModelIds.length - 1, i + 1)); return; } + if (key.name === "return") { + const sel = filteredModelIds[modelPickerIndex]; if (sel) { agent.setModel(sel); setModel(sel); saveProjectSettings({ model: sel }); } - setShowModelPicker(false); return; + setShowModelPicker(false); setModelSearchQuery(""); return; + } + if (key.name === "backspace") { + setModelSearchQuery((q) => q.slice(0, -1)); + setModelPickerIndex(0); + return; + } + if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) { + setModelSearchQuery((q) => q + key.sequence); + setModelPickerIndex(0); + return; } return; } if (isProcessing && key.name === "escape") { agent.abort(); return; } + if (key.sequence === "/" && !isProcessing) { + const text = inputRef.current?.plainText || ""; + if (!text.trim()) { setShowSlashMenu(true); setSlashMenuIndex(0); setSlashSearchQuery(""); return; } + } + if (key.name === "c" && key.ctrl) { + const text = inputRef.current?.plainText || ""; + if (text.trim()) { inputRef.current?.clear(); setPasteBlocks([]); } else { onExit ? onExit() : process.exit(0); } + return; + } if (key.name === "tab" && !isProcessing) { cycleMode(); return; } }); + const handlePaste = useCallback((event: PasteEvent) => { + const text = decodePasteBytes(event.bytes); + const trimmed = text.trim(); + const imageExts = /\.(png|jpe?g|gif|webp|svg|bmp|ico|tiff?)$/i; + if (imageExts.test(trimmed) && !trimmed.includes("\n")) { + event.preventDefault(); + const id = ++pasteCounterRef.current; + const imgNum = ++imageCounterRef.current; + setPasteBlocks((prev) => [...prev, { id, content: trimmed, lines: 1, isImage: true }]); + inputRef.current?.insertText(`[Image ${imgNum}]`); + return; + } + const lineCount = text.split("\n").length; + if (lineCount < 2) return; + event.preventDefault(); + const id = ++pasteCounterRef.current; + setPasteBlocks((prev) => [...prev, { id, content: text, lines: lineCount }]); + inputRef.current?.insertText(`[Pasted ~${lineCount} lines]`); + }, []); + const handleSubmit = useCallback(() => { - const v = inputRef.current?.value || ""; - if (!v.trim()) return; - if (inputRef.current) inputRef.current.value = ""; - if (handleCommand(v)) return; - processMessage(v); - }, [handleCommand, processMessage]); + const raw = inputRef.current?.plainText || ""; + if (!raw.trim() && pasteBlocks.length === 0) return; + inputRef.current?.clear(); + let message = raw; + const blocks = [...pasteBlocks]; + let imgIdx = 0; + setPasteBlocks([]); + for (const block of blocks) { + if (block.isImage) { + imgIdx++; + message = message.replace(`[Image ${imgIdx}]`, block.content); + } else { + message = message.replace(`[Pasted ~${block.lines} lines]`, block.content); + } + } + if (!message.trim()) return; + if (handleCommand(message)) return; + processMessage(message); + }, [handleCommand, processMessage, pasteBlocks]); const hasMessages = messages.length > 0 || streamContent || isProcessing; @@ -156,7 +305,7 @@ export function App({ agent, initialMessage }: AppProps) { {hasMessages ? ( {/* Session header — ┃ left-border panel like OpenCode's Header */} - + {/* Scrollable messages */} {messages.map((msg, i) => ( @@ -185,8 +334,9 @@ export function App({ agent, initialMessage }: AppProps) { {/* Prompt */} - + ) : ( @@ -195,41 +345,35 @@ export function App({ agent, initialMessage }: AppProps) { - {" ✦ G R O K"} - - {"AI coding agent for the terminal"} + {GROK_LOGO} - + - - {"tab"}{" cycle modes"} - {"/model"}{" switch"} - {"/quit"}{" exit"} - - {agent.getCwd()} + {agent.getCwd().replace(os.homedir(), "~")} - {"grok-cli v1.0"} + {"v1.0.0"} )} - {showModelPicker && } + {showSlashMenu && } + {showModelPicker && } ); } /* ── Session Header ──────────────────────────────────────────── */ -function SessionHeader({ t, model, modelInfo, modeInfo }: { - t: Theme; model: string; modelInfo: ReturnType; - modeInfo: typeof MODES[number]; +function SessionHeader({ t, modeInfo, sessionTitle }: { + t: Theme; modeInfo: typeof MODES[number]; sessionTitle: string | null; }) { return ( @@ -238,7 +382,10 @@ function SessionHeader({ t, model, modelInfo, modeInfo }: { border={["left"]} customBorderChars={SPLIT} borderColor={t.border} backgroundColor={t.backgroundPanel} > - {"# "}{modelInfo?.name || model} + + {modeInfo.label} + {sessionTitle ? {": "}{sessionTitle} : null} + ); @@ -246,40 +393,48 @@ function SessionHeader({ t, model, modelInfo, modeInfo }: { /* ── Prompt Box ──────────────────────────────────────────────── */ -function PromptBox({ t, inputRef, isProcessing, showModelPicker, onSubmit, mode, modeInfo, model, modelInfo, placeholder }: { - t: Theme; inputRef: React.RefObject; - isProcessing: boolean; showModelPicker: boolean; onSubmit: () => void; - mode: AgentMode; modeInfo: typeof MODES[number]; model: string; +const TEXTAREA_KEYBINDINGS: KeyBinding[] = [ + { name: "return", action: "submit" }, + { name: "return", shift: true, action: "newline" }, +]; + +function PromptBox({ t, inputRef, isProcessing, showModelPicker, showSlashMenu, onSubmit, onPaste, pasteBlocks, modeInfo, model, modelInfo, placeholder }: { + t: Theme; inputRef: React.RefObject; + isProcessing: boolean; showModelPicker: boolean; showSlashMenu: boolean; onSubmit: () => void; + onPaste: (event: PasteEvent) => void; pasteBlocks: { id: number; content: string; lines: number }[]; + modeInfo: typeof MODES[number]; model: string; modelInfo: ReturnType; placeholder?: string; }) { return ( - - - + +