diff --git a/src/core/harness.ts b/src/core/harness.ts
index 6bcee6b..ae03e41 100644
--- a/src/core/harness.ts
+++ b/src/core/harness.ts
@@ -741,6 +741,18 @@ export class Harness {
+ async chatDirect(line: string) {
+ this.stopFlag = false;
+ this.runActive = true;
+ this.events.emit({ type: "run_start", prompt: line });
+ this.agents.get("orchestrator")!.assign(
+ `[mode=chat] Answer the user's question directly. Keep your response concise, clear, and helpful. You have access to tools to read the workspace and files, but do not make any multi-agent plans unless specifically requested.\n\nUser Question:\n${line}`
+ );
+ await this.waitUntilIdle(8 * 60 * 1000);
+ this.runActive = false;
+ this.events.emit({ type: "run_end", ok: true });
+ }
+
chat(line: string) {
// During questions phase, treat as answers
if (this.phase === "questions") {
@@ -781,13 +793,13 @@ export class Harness {
return;
}
- // default: if idle, start plan; if executing, route to orch
+ // default: if idle, start standard chat direct; if executing, route to orch
if (
this.phase === "idle" ||
this.phase === "accepted" ||
this.phase === "stopped"
) {
- void this.startPlan(line);
+ void this.chatDirect(line);
return;
}
this.agents.get("orchestrator")!.assign(line);
diff --git a/src/index.ts b/src/index.ts
index a89d8e4..f295c0a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -287,11 +287,16 @@ async function main() {
process.exit(0);
}
- if (line.startsWith("/")) {
+ let cmdLine = line;
+ if (cmdLine.startsWith(".")) {
+ cmdLine = "/" + cmdLine.slice(1);
+ }
+
+ if (cmdLine.startsWith("/")) {
// Dispatch command
- const [cmd, ..._rest] = line.slice(1).split(/\s+/);
+ const [cmd, ..._rest] = cmdLine.slice(1).split(/\s+/);
const { dispatchCommand } = await import("./commands/registry");
- const res = await dispatchCommand(line, {
+ const res = await dispatchCommand(cmdLine, {
harness,
setYolo: () => {},
getYolo: () => false,
diff --git a/src/tui/App.tsx b/src/tui/App.tsx
index 9444611..6cefd2f 100644
--- a/src/tui/App.tsx
+++ b/src/tui/App.tsx
@@ -55,6 +55,59 @@ type Overlay =
type LayoutMode = "dashboard" | "classic";
+function ThinkingSpinner() {
+ const [frame, setFrame] = useState(0);
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setFrame((f) => (f + 1) % frames.length);
+ }, 80);
+ return () => clearInterval(timer);
+ }, []);
+
+ return {frames[frame]} ;
+}
+
+function renderMessageText(text: string, panelWidth: number) {
+ if (!text) return null;
+ const parts = text.split("```");
+ return parts.map((part, index) => {
+ const isCode = index % 2 === 1;
+ if (isCode) {
+ const lines = part.split("\n");
+ const firstLine = lines[0] || "";
+ const hasLang = /^[a-zA-Z0-9_-]+$/.test(firstLine.trim());
+ const codeLines = hasLang ? lines.slice(1) : lines;
+ const codeText = codeLines.join("\n").trim();
+ return (
+
+ {hasLang && (
+
+ {`/* ${firstLine.trim()} */`}
+
+ )}
+ {codeText}
+
+ );
+ } else {
+ return (
+
+ {part}
+
+ );
+ }
+ });
+}
+
function emptyAgent(id: AgentId): AgentPaneModel {
return {
id,
@@ -79,6 +132,7 @@ export function App(props: {
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 [attachedFiles, setAttachedFiles] = useState<{ path: string; content: string }[]>([]);
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" },
@@ -366,22 +420,65 @@ export function App(props: {
}
});
- const onSubmit = async (line: string) => {
+ const onSubmit = async (inputLine: string) => {
+ let line = inputLine.trim();
+ if (line.startsWith(".")) {
+ line = "/" + line.slice(1);
+ }
+
// Save to user chat log too
setChatLines((prev) => [
...prev.slice(-100),
{
id: `user-${Date.now()}-${Math.random()}`,
sender: "user",
- text: line,
+ text: inputLine,
color: "white",
},
]);
+ if (line.startsWith("/add ")) {
+ const filePath = line.slice(5).trim();
+ try {
+ const absolutePath = harness.workspace.resolve(filePath, { mustExist: true });
+ const fs = require("node:fs");
+ const content = fs.readFileSync(absolutePath, "utf8");
+ const lineCount = content.split("\n").length;
+ setAttachedFiles((prev) => [...prev, { path: filePath, content }]);
+ setSystemLine(`Attached file ${filePath} (${lineCount} lines)`);
+ setChatLines((prev) => [
+ ...prev,
+ {
+ id: `add-${Date.now()}`,
+ sender: "system",
+ text: `📎 Attached file: ${filePath} (${lineCount} lines). This will be included in your next prompt/plan.`,
+ color: "green",
+ },
+ ]);
+ } catch (err: any) {
+ setSystemLine(`Error reading file: ${err.message}`);
+ setChatLines((prev) => [
+ ...prev,
+ {
+ id: `add-err-${Date.now()}`,
+ sender: "system",
+ text: `❌ Failed to attach file "${filePath}": ${err.message}`,
+ color: "red",
+ },
+ ]);
+ }
+ return;
+ }
+
if (line.startsWith("/")) {
// local dashboard commands
const [cmd, ...rest] = line.slice(1).split(/\s+/);
const c = (cmd || "").toLowerCase();
+ if (c === "swarm") {
+ setLayoutMode((prev) => (prev === "dashboard" ? "classic" : "dashboard"));
+ setSystemLine(`Layout switched to ${layoutMode === "dashboard" ? "classic" : "dashboard"}`);
+ return;
+ }
if (c === "replay") {
const path = harness.exportReplay(rest[0]);
setSystemLine(`Replay exported → ${path}`);
@@ -404,7 +501,16 @@ export function App(props: {
return;
}
- const res = await dispatchCommand(line, {
+ let cmdLine = line;
+ if (c === "plan" && attachedFiles.length > 0) {
+ const attachmentBlock = attachedFiles
+ .map((f) => `=== ATTACHED FILE: ${f.path} ===\n${f.content}\n===============================`)
+ .join("\n\n");
+ cmdLine = `/plan ${attachmentBlock}\n\nUser Request:\n${line.slice(5).trim()}`;
+ setAttachedFiles([]);
+ }
+
+ const res = await dispatchCommand(cmdLine, {
harness,
setYolo,
getYolo: () => yolo,
@@ -444,7 +550,16 @@ export function App(props: {
refreshFiles();
return;
}
- harness.chat(line);
+
+ let payload = line;
+ if (attachedFiles.length > 0) {
+ const attachmentBlock = attachedFiles
+ .map((f) => `=== ATTACHED FILE: ${f.path} ===\n${f.content}\n===============================`)
+ .join("\n\n");
+ payload = `${attachmentBlock}\n\nUser Prompt:\n${line}`;
+ setAttachedFiles([]);
+ }
+ harness.chat(payload);
};
const onSettingsSave = (v: SettingsValues) => {
@@ -721,74 +836,28 @@ export function App(props: {
) : (
/* CLASSIC SIMPLE CHAT MODE */
-
- {/* LEFT CHAT PANEL: Beautiful classic scrollable Chat stream */}
+
+ {/* FULL SCREEN CHAT PANEL: Beautiful classic scrollable Chat stream */}
- {chatLines.slice(-Math.floor(midH / 2)).map((line) => (
-
+ {chatLines.slice(-10).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()}
-
-
+
+ {renderMessageText(line.text, width)}
+
+
))}
-
-
-
- Toggle UI mode with: /layout
-
-
)}
@@ -803,10 +872,16 @@ export function App(props: {
-
+
{systemLine}
+ {runActive && (
+
+
+ Thinking & executing tools...
+
+ )}
{finalText ? (
diff --git a/src/tui/components/InputBar.tsx b/src/tui/components/InputBar.tsx
index a18a55b..399350a 100644
--- a/src/tui/components/InputBar.tsx
+++ b/src/tui/components/InputBar.tsx
@@ -1,7 +1,35 @@
import React, { useState } from "react";
-import { Box, Text } from "ink";
+import { Box, Text, useInput } from "ink";
import TextInput from "ink-text-input";
+const SUGGESTED_COMMANDS = [
+ "/plan",
+ "/swarm",
+ "/confirm",
+ "/accept",
+ "/reject",
+ "/stop",
+ "/help",
+ "/exit",
+ "/add",
+ "/settings",
+ "/status",
+ "/cost",
+ "/clear",
+ "/goal",
+ ".plan",
+ ".swarm",
+ ".confirm",
+ ".accept"
+];
+
+function getSuggestion(val: string): string {
+ if (!val) return "";
+ const lowercaseVal = val.toLowerCase();
+ const match = SUGGESTED_COMMANDS.find((cmd) => cmd.toLowerCase().startsWith(lowercaseVal) && cmd.toLowerCase() !== lowercaseVal);
+ return match ? match.slice(val.length) : "";
+}
+
export function InputBar(props: {
width: number;
disabled?: boolean;
@@ -13,6 +41,18 @@ export function InputBar(props: {
const [value, setValue] = useState("");
const { approval } = props;
+ const suggestion = getSuggestion(value);
+
+ useInput((input, key) => {
+ if (!props.disabled && !approval) {
+ if (key.tab || key.rightArrow) {
+ if (suggestion) {
+ setValue((v) => v + suggestion);
+ }
+ }
+ }
+ });
+
if (approval) {
return (
{"prompt> "}
- {
- const t = v.trim();
- if (!t) return;
- props.onSubmit(t);
- setValue("");
- }}
- placeholder={
- props.placeholder ||
- "describe a task | /help /yolo /exit @fe @be @qa @orch"
- }
- focus={!props.disabled}
- />
+
+ {
+ const t = v.trim();
+ if (!t) return;
+ props.onSubmit(t);
+ setValue("");
+ }}
+ placeholder={
+ props.placeholder ||
+ "describe a task | /help /yolo /exit @fe @be @qa @orch"
+ }
+ focus={!props.disabled}
+ />
+ {suggestion ? (
+
+ {suggestion}
+
+ ) : null}
+
);
}