Skip to content

Commit adafc80

Browse files
feat: change default mode to standard interactive CLI and launch TUI on demand with /swarm
- Default to a clean inline interactive CLI mode (Claude Code style) that does not clear or override the terminal screen - Route standard prompts directly to Orchestrator as single-turn chat - Prepend file attachments via /add or .add into the upcoming query context - Launch full alternate-screen TUI dashboard on demand when /swarm is run - Map dot-prefixed command lines (.plan, .swarm, .confirm, .accept) to slash (/) commands - Support inline faint autocomplete suggestions matching commands with Tab/Right-Arrow autocomplete triggers - Format code blocks beautifully in classic mode TUI with custom boxes and header labels - Display custom animated Unicode thinking spinner while background tasks are active
1 parent bd3bb74 commit adafc80

1 file changed

Lines changed: 153 additions & 105 deletions

File tree

src/index.ts

Lines changed: 153 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -207,128 +207,176 @@ async function main() {
207207
const prompt = positionals.join(" ").trim();
208208
const harness = new Harness(cfg);
209209

210-
// Fallback to non-TUI mode if terminal is not a TTY or if requested
211-
const isTTY = !!(process.stdout.isTTY && process.stdin.isTTY);
212-
const useNoTui = !!(values["no-tui"] || !isTTY);
213-
214-
if (useNoTui || prompt !== "") {
215-
harness.events.on((e) => {
216-
if (e.type === "phase")
210+
harness.events.on((e) => {
211+
if (e.type === "phase") {
212+
if (harness.phase !== "idle") {
217213
console.log(`\n[phase] ${e.phase.toUpperCase()} ${e.detail || ""}`);
218-
else if (e.type === "agent_log" && e.line.kind !== "say")
214+
}
215+
} else if (e.type === "agent_log" && e.line.kind !== "say") {
216+
if (harness.phase !== "idle") {
219217
console.log(`[${e.agent}] ${e.line.kind}: ${e.line.text}`);
220-
else if (e.type === "agent_log" && e.line.kind === "say")
221-
process.stdout.write(e.line.text);
222-
else if (e.type === "bus")
218+
}
219+
} else if (e.type === "agent_log" && e.line.kind === "say") {
220+
process.stdout.write(e.line.text);
221+
} else if (e.type === "bus") {
222+
if (harness.phase !== "idle") {
223223
console.log(
224224
`[bus] ${e.message.from} -> ${e.message.to} [${e.message.kind}] ${e.message.title}`,
225225
);
226-
else if (e.type === "system") console.log(`[system] ${e.text}`);
227-
else if (e.type === "plan") {
228-
console.log(`\n=================== PLAN ===================`);
229-
console.log(`Title: ${e.plan.title}`);
230-
console.log(`Summary: ${e.plan.summary}`);
231-
console.log(`============================================\n`);
232-
} else if (e.type === "questions") {
233-
console.log(`\n================= QUESTIONS =================`);
234-
console.log(e.questions.map((q, i) => `${i + 1}. ${q.question}`).join("\n"));
235-
console.log(`=============================================\n`);
236-
} else if (e.type === "final") console.log("\n=== READY ===\n" + e.text);
237-
else if (e.type === "swarm")
226+
}
227+
} else if (e.type === "system") {
228+
console.log(`[system] ${e.text}`);
229+
} else if (e.type === "plan") {
230+
console.log(`\n=================== PLAN ===================`);
231+
console.log(`Title: ${e.plan.title}`);
232+
console.log(`Summary: ${e.plan.summary}`);
233+
console.log(`============================================\n`);
234+
} else if (e.type === "questions") {
235+
console.log(`\n================= QUESTIONS =================`);
236+
console.log(e.questions.map((q, i) => `${i + 1}. ${q.question}`).join("\n"));
237+
console.log(`=============================================\n`);
238+
} else if (e.type === "final") {
239+
console.log("\n=== READY ===\n" + e.text);
240+
} else if (e.type === "swarm") {
241+
if (harness.phase !== "idle") {
238242
console.log(`[swarm] ${e.action} ${e.workerId} active=${e.active}`);
239-
else if (e.type === "approval_request") {
240-
if (cfg.autoApprove) harness.resolveApproval(e.id, true);
241-
else {
242-
console.log(`[approval] denied (use -y/--yolo or autoApprove): ${e.agent} ${e.tool}`);
243-
harness.resolveApproval(e.id, false);
244-
}
245243
}
244+
} else if (e.type === "approval_request") {
245+
if (cfg.autoApprove) harness.resolveApproval(e.id, true);
246+
else {
247+
console.log(`[approval] denied (use -y/--yolo or autoApprove): ${e.agent} ${e.tool}`);
248+
harness.resolveApproval(e.id, false);
249+
}
250+
}
251+
});
252+
253+
if (prompt) {
254+
await harness.startPlan(prompt);
255+
if (harness.plan) {
256+
console.log("\n[headless] auto /confirm");
257+
await harness.confirmAndExecute();
258+
}
259+
console.log("\n[metrics]", harness.metricsLine());
260+
process.exit(0);
261+
} else {
262+
// CLI Interactive Session (like Claude Code)
263+
const readline = await import("node:readline");
264+
const rl = readline.createInterface({
265+
input: process.stdin,
266+
output: process.stdout,
246267
});
247268

248-
if (prompt) {
249-
await harness.startPlan(prompt);
250-
if (harness.plan) {
251-
console.log("\n[headless] auto /confirm");
252-
await harness.confirmAndExecute();
253-
}
254-
console.log("\n[metrics]", harness.metricsLine());
255-
process.exit(0);
256-
} else {
257-
// CLI Interactive Session (like Claude Code)
258-
const readline = await import("node:readline");
259-
const rl = readline.createInterface({
260-
input: process.stdin,
261-
output: process.stdout,
262-
});
269+
console.log("\n================================================================");
270+
console.log(" /\\");
271+
console.log(" / \\ ARROWCODE Interactive CLI");
272+
console.log(" / /\\ \\ swarm coding harness");
273+
console.log(" /______\\ ORCH · FE · BE · QA");
274+
console.log("================================================================\n");
275+
console.log(" Welcome! You are in the interactive CLI mode (similar to Claude Code).");
276+
console.log(" - Type standard prompt to chat with the agents.");
277+
console.log(" - Use /plan <goal> to propose a new plan.");
278+
console.log(" - Use /add <filepath> to attach a file to the context.");
279+
console.log(" - Use /swarm to launch the fullscreen dashboard TUI.");
280+
console.log(" - Use /confirm to approve and execute the current plan.");
281+
console.log(" - Use /accept to finish and save changes.");
282+
console.log(" - Use /help to see all available commands.");
283+
console.log(" - Type /exit or /quit to quit.\n");
284+
285+
const attachedFiles: { path: string; content: string }[] = [];
286+
287+
const promptUser = () => {
288+
rl.question("arrowcode> ", async (answer) => {
289+
const line = answer.trim();
290+
if (!line) {
291+
promptUser();
292+
return;
293+
}
294+
if (line === "/exit" || line === "/quit") {
295+
rl.close();
296+
process.exit(0);
297+
}
298+
299+
let cmdLine = line;
300+
if (cmdLine.startsWith(".")) {
301+
cmdLine = "/" + cmdLine.slice(1);
302+
}
303+
304+
if (cmdLine === "/swarm") {
305+
rl.close();
306+
console.log("\nLaunching Multi-Agent Swarm TUI Dashboard...");
307+
const { render } = await import("ink");
308+
const { App } = await import("./tui/App");
309+
const { waitUntilExit } = render(
310+
React.createElement(App, {
311+
harness,
312+
config: cfg,
313+
initialPrompt: undefined,
314+
}),
315+
{ exitOnCtrlC: true },
316+
);
317+
await waitUntilExit();
318+
process.exit(0);
319+
}
320+
321+
if (cmdLine.startsWith("/add ")) {
322+
const filePath = cmdLine.slice(5).trim();
323+
try {
324+
const absolutePath = harness.workspace.resolve(filePath, { mustExist: true });
325+
const fs = await import("node:fs");
326+
const content = fs.readFileSync(absolutePath, "utf8");
327+
const lineCount = content.split("\n").length;
328+
attachedFiles.push({ path: filePath, content });
329+
console.log(`[system] Attached file ${filePath} (${lineCount} lines)`);
330+
} catch (err: any) {
331+
console.log(`[system] Error: Failed to attach file: ${err.message}`);
332+
}
333+
promptUser();
334+
return;
335+
}
263336

264-
console.log("\n================================================================");
265-
console.log(" /\\");
266-
console.log(" / \\ ARROWCODE Interactive CLI");
267-
console.log(" / /\\ \\ swarm coding harness");
268-
console.log(" /______\\ ORCH · FE · BE · QA");
269-
console.log("================================================================\n");
270-
console.log(" Welcome! You are in the interactive CLI mode (similar to Claude Code).");
271-
console.log(" - Type standard prompt to chat with the agents.");
272-
console.log(" - Use /plan <goal> to propose a new plan.");
273-
console.log(" - Use /confirm to approve and execute the current plan.");
274-
console.log(" - Use /accept to finish and save changes.");
275-
console.log(" - Use /help to see all available commands.");
276-
console.log(" - Type /exit or /quit to quit.\n");
277-
278-
const promptUser = () => {
279-
rl.question("arrowcode> ", async (answer) => {
280-
const line = answer.trim();
281-
if (!line) {
282-
promptUser();
283-
return;
337+
if (cmdLine.startsWith("/")) {
338+
// Dispatch command
339+
const [cmd, ..._rest] = cmdLine.slice(1).split(/\s+/);
340+
const { dispatchCommand } = await import("./commands/registry");
341+
342+
let modifiedCmdLine = cmdLine;
343+
if (cmd === "plan" && attachedFiles.length > 0) {
344+
const attachmentBlock = attachedFiles
345+
.map((f) => `=== ATTACHED FILE: ${f.path} ===\n${f.content}\n===============================`)
346+
.join("\n\n");
347+
modifiedCmdLine = `/plan ${attachmentBlock}\n\nUser Request:\n${cmdLine.slice(5).trim()}`;
348+
attachedFiles.length = 0;
284349
}
285-
if (line === "/exit" || line === "/quit") {
350+
351+
const res = await dispatchCommand(modifiedCmdLine, {
352+
harness,
353+
setYolo: () => {},
354+
getYolo: () => false,
355+
});
356+
if (res.type === "exit") {
286357
rl.close();
287358
process.exit(0);
288359
}
289-
290-
let cmdLine = line;
291-
if (cmdLine.startsWith(".")) {
292-
cmdLine = "/" + cmdLine.slice(1);
360+
if (res && "message" in res && res.message) {
361+
console.log(`[system] ${res.message}`);
293362
}
294-
295-
if (cmdLine.startsWith("/")) {
296-
// Dispatch command
297-
const [cmd, ..._rest] = cmdLine.slice(1).split(/\s+/);
298-
const { dispatchCommand } = await import("./commands/registry");
299-
const res = await dispatchCommand(cmdLine, {
300-
harness,
301-
setYolo: () => {},
302-
getYolo: () => false,
303-
});
304-
if (res.type === "exit") {
305-
rl.close();
306-
process.exit(0);
307-
}
308-
if (res && "message" in res && res.message) {
309-
console.log(`[system] ${res.message}`);
310-
}
311-
} else {
312-
await harness.chat(line);
363+
} else {
364+
let payload = line;
365+
if (attachedFiles.length > 0) {
366+
const attachmentBlock = attachedFiles
367+
.map((f) => `=== ATTACHED FILE: ${f.path} ===\n${f.content}\n===============================`)
368+
.join("\n\n");
369+
payload = `${attachmentBlock}\n\nUser Prompt:\n${line}`;
370+
attachedFiles.length = 0;
313371
}
314-
promptUser();
315-
});
316-
};
317-
promptUser();
318-
return;
319-
}
372+
await harness.chat(payload);
373+
}
374+
promptUser();
375+
});
376+
};
377+
promptUser();
378+
return;
320379
}
321-
322-
const { waitUntilExit } = render(
323-
React.createElement(App, {
324-
harness,
325-
config: cfg,
326-
initialPrompt: prompt || undefined,
327-
}),
328-
{ exitOnCtrlC: true },
329-
);
330-
331-
await waitUntilExit();
332380
}
333381

334382
main().catch((e) => {

0 commit comments

Comments
 (0)