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;