From 9296ad22d0b8d02bc97be67ec2c712ae9de5158d Mon Sep 17 00:00:00 2001 From: Chintanpatel24 <216989679+Chintanpatel24@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:05:00 +0000 Subject: [PATCH] feat: implement classic chat layout mode and offline mock/demo mode - Create beautiful high-contrast monochrome 'Classic Chat Mode' in the TUI as the default layout. - Allow simple toggling between classic chat and advanced dashboard modes via `/layout` or `/ui` command. - Implement a fully local offline 'demo/mock' provider mode for immediate quickstart without real API keys. - Update onboarding startup wizard to let users instantly try ArrowCode with Mock Mode or set up a real LLM. --- src/config/load.ts | 16 ++- src/config/types.ts | 2 +- src/index.ts | 46 +++++-- src/llm/client.ts | 56 ++++++++- src/tui/App.tsx | 215 ++++++++++++++++++++++++++------ src/tui/components/Header.tsx | 10 +- src/tui/components/InputBar.tsx | 6 +- 7 files changed, 289 insertions(+), 62 deletions(-) diff --git a/src/config/load.ts b/src/config/load.ts index 2c78c16..b571dad 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -96,10 +96,11 @@ export function isConfigured(): boolean { process.env.NIM_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || - process.env.ARROWCODE_API_KEY; + process.env.ARROWCODE_API_KEY || + process.env.DEMO_API_KEY; if (key) return true; - if (process.env.ARROWCODE_PROVIDER === "ollama") return true; + if (process.env.ARROWCODE_PROVIDER === "ollama" || process.env.ARROWCODE_PROVIDER === "demo" || process.env.ARROWCODE_PROVIDER === "mock") return true; for (const p of ["ORCH", "FE", "BE", "QA"]) { if (process.env[`ARROW_${p}_API_KEY`]) return true; @@ -108,7 +109,7 @@ export function isConfigured(): boolean { if (existsSync(CONFIG_PATH)) { try { const c = parseYaml(readFileSync(CONFIG_PATH, "utf8")) as Partial; - if (c.provider === "ollama" || c.apiKey) return true; + if (c.provider === "ollama" || c.provider === "demo" || c.provider === "mock" || c.apiKey) return true; } catch { /* */ } @@ -122,6 +123,13 @@ function providerDefaults(provider: ProviderId): { envKey: string; } { switch (provider) { + case "demo": + case "mock": + return { + baseUrl: "demo", + model: "demo-v1", + envKey: "DEMO_API_KEY", + }; case "nim": return { baseUrl: DEFAULT_NIM_BASE, @@ -314,7 +322,7 @@ export function loadConfig(overrides: Partial = {}): ArrowConfig { process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || file.apiKey || - (provider === "ollama" ? "ollama" : ""); + (provider === "ollama" ? "ollama" : (provider === "demo" || provider === "mock" ? "demo" : "")); const baseUrl = overrides.baseUrl || diff --git a/src/config/types.ts b/src/config/types.ts index b5d4a2a..f865634 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,6 +1,6 @@ /** Shared types for ArrowCode — swarm multi-agent coding harness. */ -export type ProviderId = "nim" | "openai" | "anthropic" | "ollama" | "groq" | "deepseek" | "gemini" | "openrouter" | "custom"; +export type ProviderId = "nim" | "openai" | "anthropic" | "ollama" | "groq" | "deepseek" | "gemini" | "openrouter" | "custom" | "demo" | "mock"; export type AgentId = "orchestrator" | "frontend" | "backend" | "tester"; diff --git a/src/index.ts b/src/index.ts index 8552b10..a89d8e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -138,14 +138,46 @@ async function main() { if (!isConfigured()) { printBanner({ compact: true }); console.log("ArrowCode is not configured yet.\n"); - console.log( - "This will create ~/.arrowcode from packaged defaults/ and ask for an API key.\n", - ); + console.log("Welcome to ArrowCode! Since you don't have an API key configured yet,"); + console.log("you can choose one of the following options to get started immediately:"); + console.log(" 1) Use Mock / Demo mode (free, runs locally, no keys needed)"); + console.log(" 2) Configure a real LLM provider (NVIDIA NIM, OpenAI, Anthropic, etc.)"); + console.log(""); + const readline = await import("node:readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + const choice = await new Promise((res) => { + rl.question("Choose [1-2] (default 1): ", (ans) => res((ans || "").trim())); + }); + rl.close(); + bootstrapUserHome(); - const ok = await runSetupWizard(); - if (!ok) process.exit(1); - if (!isConfigured()) { - process.exit(0); + + if (choice === "2") { + const ok = await runSetupWizard(); + if (!ok) process.exit(1); + if (!isConfigured()) { + process.exit(0); + } + } else { + // Setup demo/mock mode + const { saveEnv, saveConfig } = await import("./config/load"); + saveEnv({ + ARROWCODE_PROVIDER: "demo", + ARROWCODE_MODEL: "demo-v1", + ARROWCODE_BASE_URL: "demo", + DEMO_API_KEY: "demo", + }); + saveConfig({ + provider: "demo", + model: "demo-v1", + baseUrl: "demo", + apiKey: "demo", + }); + console.log("\nDemo Mode configured! Starting ArrowCode...\n"); + await new Promise((resolve) => setTimeout(resolve, 1000)); } } diff --git a/src/llm/client.ts b/src/llm/client.ts index 1c86640..8920846 100644 --- a/src/llm/client.ts +++ b/src/llm/client.ts @@ -36,14 +36,14 @@ export class LLMClient { this.endpoint = endpoint; this.model = endpoint.model; this.provider = endpoint.provider; - if (!endpoint.apiKey && endpoint.provider !== "ollama") { + if (!endpoint.apiKey && endpoint.provider !== "ollama" && endpoint.provider !== "demo" && endpoint.provider !== "mock") { throw new LLMError( "No API key for this agent. Set global key or ARROW__API_KEY / agents.yaml", ); } this.client = new OpenAI({ apiKey: endpoint.apiKey || "ollama", - baseURL: endpoint.baseUrl, + baseURL: endpoint.provider === "demo" || endpoint.provider === "mock" ? "http://demo-url" : endpoint.baseUrl, timeout: 120_000, maxRetries: 0, }); @@ -54,6 +54,58 @@ export class LLMClient { tools?: OpenAITool[], opts?: { onToken?: (t: string) => void; stream?: boolean }, ): Promise { + if (this.provider === "demo" || this.provider === "mock") { + // Mock LLM response generator for instant works/tryout! + const userMessage = messages[messages.length - 1]?.content || ""; + let replyContent = "Let's explore the workspace and carry out your request."; + const toolCalls: ToolCall[] = []; + + // Determine the kind of reply based on the message content + const lower = typeof userMessage === "string" ? userMessage.toLowerCase() : ""; + if (lower.includes("[mode=plan]") || lower.includes("plan")) { + replyContent = `\`\`\`arrow-plan +title: Demo Implementation Plan +summary: A mockup flow to demonstrate clean and fast execution. +steps: + 1. Explore local directories and files. + 2. Setup frontend core elements. + 3. Validate using offline tests. +risks: + - Mock mode doesn't execute actual remote files. +acceptance: + - UI works fine. + - Tests pass with green indicators. +agents: + frontend: Core UI setup + backend: API endpoints mocking + tester: Verify everything +\`\`\``; + } else if (lower.includes("[execute]") || lower.includes("execute") || lower.includes("confirm")) { + replyContent = `\`\`\`arrow-ready +Verification: Verified that all mock procedures ran fine and the application is highly optimized. +All tests are passing. +\`\`\``; + } else if (lower.includes("health") || lower.includes("status")) { + replyContent = "Everything is functioning correctly under the demo environment."; + } + + if (opts?.onToken) { + // simulate streaming + const parts = replyContent.split(" "); + for (const p of parts) { + opts.onToken(p + " "); + await Bun.sleep(5); + } + } + + return { + content: replyContent, + toolCalls, + finishReason: "stop", + usage: { prompt_tokens: messages.length * 10, completion_tokens: replyContent.length / 4 } + }; + } + const stream = Boolean(opts?.stream && opts.onToken); const body: OpenAI.Chat.ChatCompletionCreateParams = { model: this.model, diff --git a/src/tui/App.tsx b/src/tui/App.tsx index 23b6f88..9444611 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -53,6 +53,8 @@ type Overlay = | "plan" | "replay"; +type LayoutMode = "dashboard" | "classic"; + function emptyAgent(id: AgentId): AgentPaneModel { return { id, @@ -76,6 +78,11 @@ export function App(props: { const { stdout } = useStdout(); const [cols, setCols] = useState(stdout?.columns || 120); const [rows, setRows] = useState(stdout?.rows || 40); + const [layoutMode, setLayoutMode] = useState("classic"); // Default is simple/classic chat interface! + const [chatLines, setChatLines] = useState<{ id: string; sender: string; text: string; color?: string }[]>([ + { id: "welcome-1", sender: "system", text: "Welcome to ArrowCode Classic Mode!", color: "cyan" }, + { id: "welcome-2", sender: "system", text: "Type any prompt or /help to explore. Toggle layout with /layout.", color: "gray" }, + ]); const [agents, setAgents] = useState>(() => { const o = {} as Record; for (const id of AGENT_ORDER) o[id] = emptyAgent(id); @@ -145,6 +152,17 @@ export function App(props: { const trimmed = logs.length > 400 ? logs.slice(-400) : logs; return { ...prev, [id]: { ...cur, logs: trimmed } }; }); + if (line.kind !== "think") { + setChatLines((prev) => [ + ...prev.slice(-100), + { + id: `log-${Date.now()}-${Math.random()}`, + sender: id.toUpperCase(), + text: line.text, + color: id === "orchestrator" ? "cyan" : id === "frontend" ? "magenta" : id === "backend" ? "yellow" : "green", + }, + ]); + } }, []); useEffect(() => { @@ -218,10 +236,28 @@ export function App(props: { "bus", `${e.message.from}→${e.message.to} ${e.message.title}`, ); + setChatLines((prev) => [ + ...prev.slice(-100), + { + id: `bus-${Date.now()}-${Math.random()}`, + sender: "bus", + text: `${e.message.from} -> ${e.message.to}: ${e.message.title}`, + color: "gray", + }, + ]); break; case "system": setSystemLine(e.text); harness.sessionLog.push("system", e.text); + setChatLines((prev) => [ + ...prev.slice(-100), + { + id: `sys-${Date.now()}-${Math.random()}`, + sender: "system", + text: e.text, + color: "yellow", + }, + ]); break; case "run_start": setRunActive(true); @@ -236,6 +272,15 @@ export function App(props: { case "final": setFinalText(e.text); harness.sessionLog.push("final", e.text.slice(0, 200)); + setChatLines((prev) => [ + ...prev.slice(-100), + { + id: `final-${Date.now()}-${Math.random()}`, + sender: "ready", + text: e.text, + color: "green", + }, + ]); break; case "approval_request": setApproval({ @@ -322,6 +367,17 @@ export function App(props: { }); const onSubmit = async (line: string) => { + // Save to user chat log too + setChatLines((prev) => [ + ...prev.slice(-100), + { + id: `user-${Date.now()}-${Math.random()}`, + sender: "user", + text: line, + color: "white", + }, + ]); + if (line.startsWith("/")) { // local dashboard commands const [cmd, ...rest] = line.slice(1).split(/\s+/); @@ -342,6 +398,11 @@ export function App(props: { setSystemLine("Dashboard panels refreshed"); return; } + if (c === "layout" || c === "ui") { + setLayoutMode((prev) => (prev === "dashboard" ? "classic" : "dashboard")); + setSystemLine(`Layout switched to ${layoutMode === "dashboard" ? "classic" : "dashboard"}`); + return; + } const res = await dispatchCommand(line, { harness, @@ -611,52 +672,126 @@ export function App(props: { questions={questions} /> - {/* MAIN DASHBOARD */} - - {/* LEFT: 2x2 agents */} - - - - + {layoutMode === "dashboard" ? ( + /* MAIN DASHBOARD MODE */ + + {/* LEFT: 2x2 agents */} + + + + + + + + + - - - + + {/* RIGHT: plan | swarm / files | diff */} + + + + + + + + + - - {/* RIGHT: plan | swarm / files | diff */} - - - - + ) : ( + /* CLASSIC SIMPLE CHAT MODE */ + + {/* LEFT CHAT PANEL: Beautiful classic scrollable Chat stream */} + + + {chatLines.slice(-Math.floor(midH / 2)).map((line) => ( + + + {`[${line.sender}]`} + + {line.text.slice(0, leftW - 12)} + + ))} + - - - + + {/* RIGHT SIDEBAR: Sleek classic high-contrast status card */} + + ┌── STATUS PANEL ──┐ + + + Workspace: + {config.workspace.split("/").pop() || "root"} + + + Provider: + {config.provider} + + + Active: + {phase.toUpperCase()} + + + Swarm: + {`${swarmActive} / ${config.swarm?.maxWorkers ?? 16}`} + + + + + SQUAD STATUS + {panes.map((p) => ( + + {p.id.toUpperCase().slice(0, 4)}: + + {p.status.toUpperCase()} + + + ))} + + + + + Toggle UI mode with: /layout + + - + )} {showQuestions ? ( diff --git a/src/tui/components/Header.tsx b/src/tui/components/Header.tsx index 8c3c8af..a08cdab 100644 --- a/src/tui/components/Header.tsx +++ b/src/tui/components/Header.tsx @@ -19,24 +19,24 @@ export function Header(props: { - {"▶ ARROWCODE"} + {"◆ ARROWCODE ◆"} - v1.0 + [v1.0.0] | - {config.provider} + {config.provider.toUpperCase()} / {truncate(config.model, 28)} {config.templateId ? ( {" "} - | tmpl {config.templateId} + | tmpl: {config.templateId} ) : null} diff --git a/src/tui/components/InputBar.tsx b/src/tui/components/InputBar.tsx index ea95d61..a18a55b 100644 --- a/src/tui/components/InputBar.tsx +++ b/src/tui/components/InputBar.tsx @@ -69,13 +69,13 @@ export function InputBar(props: { return ( - {"> "} + {"prompt> "}