From d53229bf9fe7f153d77edae2d4d7dbdef6d99770 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 18 Mar 2026 07:56:15 +0000 Subject: [PATCH] feat: differentiated UI, colored modes, clean logo, remove xAI labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all 'xAI' labels from model picker and prompt box - Mode colors: blue (#5c9cf5) for Agent, yellow (#e5c07b) for Plan, orange (#f5a742) for Ask - Logo: clean '✦ G R O K' text with tagline (no broken Unicode art) - Differentiated UI from OpenCode: - Home: centered logo + tagline, input with thin single-line border colored by mode, mode selector row showing all three modes with dots, model name on right - Session: top bar with model + mode instead of bordered header panel, thin separator line, user messages with colored ▸ prefix instead of left-border panels, input with colored single-line border instead of thick ┃ accent - Tool output in subtle background panels - Model picker: bordered modal instead of borderless floating panel, category headers in muted text, no 'xAI' provider label - Simplified theme: removed unused color tokens, added textDim for ultra-subtle text Co-authored-by: Ismail Pelaseyed --- src/types/index.ts | 6 +- src/ui/app.tsx | 375 +++++++++++++++------------------------------ src/ui/theme.ts | 22 +-- 3 files changed, 137 insertions(+), 266 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 2819f7d..7f42d61 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -64,7 +64,7 @@ export interface ModelInfo { export type AgentMode = "agent" | "plan" | "ask"; export const MODES: { id: AgentMode; label: string; color: string }[] = [ - { id: "agent", label: "Agent", color: "#ffffff" }, - { id: "plan", label: "Plan", color: "#808080" }, - { id: "ask", label: "Ask", color: "#808080" }, + { id: "agent", label: "Agent", color: "#5c9cf5" }, + { id: "plan", label: "Plan", color: "#e5c07b" }, + { id: "ask", label: "Ask", color: "#f5a742" }, ]; diff --git a/src/ui/app.tsx b/src/ui/app.tsx index bcd883e..0962a55 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -5,30 +5,9 @@ import type { Agent } from "../agent/agent.js"; import type { ChatEntry, ToolCall, AgentMode } from "../types/index.js"; import { MODES } from "../types/index.js"; import { getModelInfo, MODEL_GROUPS, MODELS } from "../grok/models.js"; -import { getAvailableModels, saveProjectSettings } from "../utils/settings.js"; +import { 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: "", -}; -const SPLIT_BORDER_END = { ...SPLIT_BORDER, bottomLeft: "╹" }; -const EMPTY_BORDER = { - topLeft: "", bottomLeft: "", vertical: "", topRight: "", - bottomRight: "", horizontal: " ", bottomT: "", topT: "", - cross: "", leftT: "", rightT: "", -}; - -const GROK_LOGO = [ - " ⢀⣴⣶⣦⡀ ", - " ⣰⣿⠟⠁⢹⣷ ", - " ⣰⣿⠋ ⢀⣿⡇ ", - " ⣿⣿ ⢀⣾⡿⠁ ", - " ⠹⣿⣦⣴⣿⠟⠁ ", - " ⠈⠛⠛⠋⠁ ", -]; - interface AppProps { agent: Agent; initialMessage?: string; @@ -59,14 +38,12 @@ 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 scrollToBottom = useCallback(() => { try { scrollRef.current?.scrollTo(scrollRef.current?.scrollHeight ?? 99999); } catch { /* */ } }, []); - // Flat model list for picker navigation - const flatModels = MODELS.map((m) => m.id); - const processMessage = useCallback(async (text: string) => { if (!text.trim() || isProcessing) return; setIsProcessing(true); setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); contentAccRef.current = ""; @@ -101,7 +78,7 @@ 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(flatModels.indexOf(model)); 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); } return false; }, [agent, model, flatModels]); @@ -123,11 +100,11 @@ export function App({ agent, initialMessage }: AppProps) { }); const handleSubmit = useCallback(() => { - const value = inputRef.current?.value || ""; - if (!value.trim()) return; + const v = inputRef.current?.value || ""; + if (!v.trim()) return; if (inputRef.current) inputRef.current.value = ""; - if (handleCommand(value)) return; - processMessage(value); + if (handleCommand(v)) return; + processMessage(v); }, [handleCommand, processMessage]); const hasMessages = messages.length > 0 || streamContent || isProcessing; @@ -135,214 +112,135 @@ export function App({ agent, initialMessage }: AppProps) { return ( {hasMessages ? ( - - ) : ( - - )} - {/* Model picker modal overlay */} - {showModelPicker && ( - - )} - - ); -} - -/* ── 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 ( - <> - - - - {/* Grok Logo */} - - - - {GROK_LOGO.map((line, i) => ( - {line} - ))} + + {/* Top bar: model on left, mode pill on right */} + + {modelInfo?.name || model}{isProcessing ? " working..." : ""} + {modeInfo.label} + + {/* Thin separator */} + {"─".repeat(Math.max(1, width - 4))} + {/* Messages */} + + {messages.map((msg, i) => )} + {activeToolCalls.map((tc, i) => ( + + {"› "}{tc.function.name === "bash" ? toolArgs(tc) : `${tc.function.name} ${toolArgs(tc)}`} + + ))} + {streamReasoning && ( + {"thinking: "}{trunc(streamReasoning, 200)} + )} + {streamContent && {streamContent}} + {isProcessing && !streamContent && activeToolCalls.length === 0 && ( + {"..."} + )} + + {/* Input area */} + + + + {/* Bottom hints */} + + {agent.getCwd()} + + {"tab"}{" mode"} + {"/model"} - {"Grok"} - - {/* Prompt box */} - - - - {/* Keyboard hints */} - - - {"tab "}{"modes"} - {"ctrl+p "}{"commands"} + ) : ( + /* ── Home Screen ──────────────────────────────────── */ + <> + + + {/* Logo: simple bold text */} + + {" ✦ G R O K"} + + {"AI coding agent for the terminal"} + + + {/* Prompt area */} + + + + + {/* Mode + model row */} + + + {MODES.map((m) => ( + + {m.id === mode ? `● ${m.label}` : m.label} + + ))} + + {modelInfo?.name || model} + + + + {/* Hints */} + + {"tab"}{" cycle modes"} + {"/model"}{" switch"} + {"/quit"}{" exit"} + + - - - - - {agent.getCwd()} - - {"v1.0.0"} - - - ); -} - -/* ── Session View ────────────────────────────────────────────── */ - -function SessionView({ t, agent, model, mode, modeInfo, modelInfo, messages, streamContent, streamReasoning, isProcessing, activeToolCalls, scrollRef, inputRef, showModelPicker, handleSubmit }: { - 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; -}) { - return ( - -
- - {messages.map((msg, i) => )} - {activeToolCalls.length > 0 && activeToolCalls.map((tc, i) => )} - {streamReasoning && ( - - {"Thinking: "}{truncate(streamReasoning, 300)} + {/* Footer */} + + {agent.getCwd()} + + {"grok-cli v1.0"} - )} - {streamContent && {streamContent}} - {isProcessing && !streamContent && activeToolCalls.length === 0 && ( - {"~ Thinking..."} - )} - - - - - - {agent.getCwd()} - {model} - - - ); -} - -/* ── 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; - modelInfo: ReturnType; placeholder?: string; -}) { - return ( - - - - - - {modeInfo.label}{" "} - {modelInfo?.name || model} - {"xAI"} - - - - - - - - {isProcessing ? ( - {"esc "}{"interrupt"} - ) : ( - <> - {"tab "}{"modes"} - {"ctrl+p "}{"commands"} - - )} - - - ); -} - -/* ── Header ──────────────────────────────────────────────────── */ - -function Header({ t, model, modelInfo, isProcessing }: { t: Theme; model: string; modelInfo: ReturnType; isProcessing: boolean }) { - return ( - - - {"# Grok CLI"} - {modelInfo ? `${modelInfo.name} · ${model}` : model}{isProcessing ? " ⏳" : ""} - + + )} + {/* Model picker overlay */} + {showModelPicker && } ); } /* ── Messages ────────────────────────────────────────────────── */ -function MessageView({ entry, index, t }: { entry: ChatEntry; index: number; t: Theme }) { +function MessageView({ entry, index, t, modeColor }: { entry: ChatEntry; index: number; t: Theme; modeColor: string }) { switch (entry.type) { case "user": return ( - - - {entry.content} - + + {"▸ "}{entry.content} ); case "assistant": - return {entry.content}; + return {entry.content}; case "tool_result": { const name = entry.toolCall?.function.name || "tool"; - const success = entry.toolResult?.success ?? true; - const args = getToolArgs(entry.toolCall); + const ok = entry.toolResult?.success ?? true; + const args = toolArgs(entry.toolCall); const output = entry.content; if (name === "bash" && output && output.split("\n").length > 3) { const lines = output.split("\n"); - const preview = lines.slice(0, 10).join("\n") + (lines.length > 10 ? "\n…" : ""); + const preview = lines.slice(0, 8).join("\n") + (lines.length > 8 ? "\n ..." : ""); return ( - - {"# Shell"} - - {"$ "}{args} - {preview} - + + {"$ "}{args} + {preview} ); } return ( - - {success ? "$" : "✗"}{" "}{name === "bash" ? args : `${name} ${args}`} + + {ok ? "$ " : "✗ "}{name === "bash" ? args : `${name} ${args}`} ); } @@ -350,63 +248,42 @@ function MessageView({ entry, index, t }: { entry: ChatEntry; index: number; t: } } -function ToolCallPending({ tc, t }: { tc: ToolCall; t: Theme }) { - const args = getToolArgs(tc); - return {"~ "}{tc.function.name === "bash" ? args : `${tc.function.name} ${args}`}; -} - -/* ── Model Picker Modal (OpenCode-style floating dialog) ─────── */ +/* ── Model Picker Modal ──────────────────────────────────────── */ function ModelPickerModal({ t, currentModel, selectedIndex, width, height }: { t: Theme; currentModel: string; selectedIndex: number; width: number; height: number; }) { let flatIdx = 0; - return ( - - - {/* Title row */} + + + {/* Header */} - {"Select model"} + {"Select model"} {"esc"} - {/* Search placeholder */} - - {"Search"} - - {/* Grouped model list */} + {/* List */} {MODEL_GROUPS.map((group) => { - const groupItems = group.models.map((mid) => { + const items = group.models.map((mid) => { const info = getModelInfo(mid); const idx = flatIdx++; - const selected = idx === selectedIndex; - const isCurrent = mid === currentModel; - return { mid, info, idx, selected, isCurrent }; + return { mid, info, idx, selected: idx === selectedIndex, current: mid === currentModel }; }); return ( - {group.category} + {group.category} - {groupItems.map(({ mid, info, selected, isCurrent }) => ( + {items.map(({ mid, info, selected, current }) => ( - {isCurrent ? "● " : " "}{info?.name || mid}{" "} - {"xAI"} + {current ? "● " : " "}{info?.name || mid} {info && info.inputPrice < 1 && ( {`$${info.inputPrice}/$${info.outputPrice}`} @@ -425,8 +302,8 @@ function ModelPickerModal({ t, currentModel, selectedIndex, width, height }: { /* ── Helpers ──────────────────────────────────────────────────── */ -function getToolArgs(tc?: ToolCall): string { +function toolArgs(tc?: ToolCall): string { if (!tc) return ""; try { const a = JSON.parse(tc.function.arguments); return tc.function.name === "bash" ? a.command || "" : a.query || ""; } catch { return ""; } } -function truncate(s: string, max: number): string { return s.length <= max ? s : s.slice(0, max) + "…"; } +function trunc(s: string, n: number): string { return s.length <= n ? s : s.slice(0, n) + "…"; } diff --git a/src/ui/theme.ts b/src/ui/theme.ts index 2f55d33..fc3f48d 100644 --- a/src/ui/theme.ts +++ b/src/ui/theme.ts @@ -1,21 +1,15 @@ export const dark = { background: "#000000", - backgroundPanel: "#141414", - backgroundElement: "#1e1e1e", - border: "#484848", - borderActive: "#606060", - borderSubtle: "#3c3c3c", - text: "#ffffff", - textMuted: "#808080", + backgroundPanel: "#111111", + backgroundElement: "#1a1a1a", + border: "#333333", + borderActive: "#555555", + text: "#e0e0e0", + textMuted: "#666666", + textDim: "#444444", primary: "#ffffff", - secondary: "#808080", - accent: "#ffffff", - error: "#ffffff", - warning: "#ffffff", - success: "#ffffff", - info: "#808080", selected: "#ffffff", - selectedBg: "#3c3c3c", + selectedBg: "#2a2a2a", } as const; export type Theme = typeof dark;