refactor(cli): centralize command execution flow (#841)

This commit is contained in:
Charles Packer
2026-02-05 18:21:07 -08:00
committed by GitHub
parent 2b7d618b39
commit 37e8347358
9 changed files with 1360 additions and 1479 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -49,6 +49,12 @@ function uid(prefix: string) {
// Helper type for command result
type CommandLine = Extract<Line, { kind: "command" }>;
let activeCommandId: string | null = null;
export function setActiveCommandId(id: string | null): void {
activeCommandId = id;
}
// Context passed to connect handlers
export interface ConnectCommandContext {
buffersRef: { current: Buffers };
@@ -66,17 +72,22 @@ function addCommandResult(
success: boolean,
phase: "running" | "finished" = "finished",
): string {
const cmdId = uid("cmd");
const cmdId = activeCommandId ?? uid("cmd");
const existing = buffersRef.current.byId.get(cmdId);
const nextInput =
existing && existing.kind === "command" ? existing.input : input;
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
input: nextInput,
output,
phase,
...(phase === "finished" && { success }),
};
buffersRef.current.byId.set(cmdId, line);
buffersRef.current.order.push(cmdId);
if (!buffersRef.current.order.includes(cmdId)) {
buffersRef.current.order.push(cmdId);
}
refreshDerived();
return cmdId;
}
@@ -91,10 +102,13 @@ function updateCommandResult(
success: boolean,
phase: "running" | "finished" = "finished",
): void {
const existing = buffersRef.current.byId.get(cmdId);
const nextInput =
existing && existing.kind === "command" ? existing.input : input;
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
input: nextInput,
output,
phase,
...(phase === "finished" && { success }),

View File

@@ -18,6 +18,12 @@ function uid(prefix: string) {
// Helper type for command result
type CommandLine = Extract<Line, { kind: "command" }>;
let activeCommandId: string | null = null;
export function setActiveCommandId(id: string | null): void {
activeCommandId = id;
}
// Context passed to MCP handlers
export interface McpCommandContext {
buffersRef: { current: Buffers };
@@ -34,17 +40,22 @@ export function addCommandResult(
success: boolean,
phase: "running" | "finished" = "finished",
): string {
const cmdId = uid("cmd");
const cmdId = activeCommandId ?? uid("cmd");
const existing = buffersRef.current.byId.get(cmdId);
const nextInput =
existing && existing.kind === "command" ? existing.input : input;
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
input: nextInput,
output,
phase,
...(phase === "finished" && { success }),
};
buffersRef.current.byId.set(cmdId, line);
buffersRef.current.order.push(cmdId);
if (!buffersRef.current.order.includes(cmdId)) {
buffersRef.current.order.push(cmdId);
}
refreshDerived();
return cmdId;
}
@@ -59,10 +70,13 @@ export function updateCommandResult(
success: boolean,
phase: "running" | "finished" = "finished",
): void {
const existing = buffersRef.current.byId.get(cmdId);
const nextInput =
existing && existing.kind === "command" ? existing.input : input;
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
input: nextInput,
output,
phase,
...(phase === "finished" && { success }),

View File

@@ -14,6 +14,12 @@ function uid(prefix: string) {
// Helper type for command result
type CommandLine = Extract<Line, { kind: "command" }>;
let activeCommandId: string | null = null;
export function setActiveCommandId(id: string | null): void {
activeCommandId = id;
}
// Context passed to profile handlers
export interface ProfileCommandContext {
buffersRef: { current: Buffers };
@@ -33,17 +39,22 @@ export function addCommandResult(
success: boolean,
phase: "running" | "finished" = "finished",
): string {
const cmdId = uid("cmd");
const cmdId = activeCommandId ?? uid("cmd");
const existing = buffersRef.current.byId.get(cmdId);
const nextInput =
existing && existing.kind === "command" ? existing.input : input;
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
input: nextInput,
output,
phase,
...(phase === "finished" && { success }),
};
buffersRef.current.byId.set(cmdId, line);
buffersRef.current.order.push(cmdId);
if (!buffersRef.current.order.includes(cmdId)) {
buffersRef.current.order.push(cmdId);
}
refreshDerived();
return cmdId;
}
@@ -58,10 +69,13 @@ export function updateCommandResult(
success: boolean,
phase: "running" | "finished" = "finished",
): void {
const existing = buffersRef.current.byId.get(cmdId);
const nextInput =
existing && existing.kind === "command" ? existing.input : input;
const line: CommandLine = {
kind: "command",
id: cmdId,
input,
input: nextInput,
output,
phase,
...(phase === "finished" && { success }),

107
src/cli/commands/runner.ts Normal file
View File

@@ -0,0 +1,107 @@
import type { MutableRefObject } from "react";
import type { Buffers, Line } from "../helpers/accumulator";
export type CommandPhase = "running" | "waiting" | "finished";
export type CommandUpdate = {
output: string;
phase?: CommandPhase;
success?: boolean;
dimOutput?: boolean;
preformatted?: boolean;
};
export type CommandHandle = {
id: string;
input: string;
update: (update: CommandUpdate) => void;
finish: (
output: string,
success?: boolean,
dimOutput?: boolean,
preformatted?: boolean,
) => void;
fail: (output: string) => void;
};
type CreateId = (prefix: string) => string;
type RunnerDeps = {
buffersRef: MutableRefObject<Buffers>;
refreshDerived: () => void;
createId: CreateId;
};
function upsertCommandLine(
buffers: Buffers,
id: string,
input: string,
update: CommandUpdate,
): void {
const existing = buffers.byId.get(id);
const next: Line = {
kind: "command",
id,
input: existing?.kind === "command" ? existing.input : input,
output: update.output,
phase: update.phase ?? "running",
success: update.success,
dimOutput: update.dimOutput,
preformatted: update.preformatted,
};
buffers.byId.set(id, next);
}
export function createCommandRunner({
buffersRef,
refreshDerived,
createId,
}: RunnerDeps) {
function getHandle(id: string, input: string): CommandHandle {
const update = (updateData: CommandUpdate) => {
upsertCommandLine(buffersRef.current, id, input, updateData);
if (!buffersRef.current.order.includes(id)) {
buffersRef.current.order.push(id);
}
refreshDerived();
};
const finish = (
finalOutput: string,
success = true,
dimOutput?: boolean,
preformatted?: boolean,
) =>
update({
output: finalOutput,
phase: "finished",
success,
dimOutput,
preformatted,
});
const fail = (finalOutput: string) =>
update({
output: finalOutput,
phase: "finished",
success: false,
});
return { id, input, update, finish, fail };
}
function start(input: string, output: string): CommandHandle {
const id = createId("cmd");
const buffers = buffersRef.current;
upsertCommandLine(buffers, id, input, {
output,
phase: "running",
});
buffers.order.push(id);
refreshDerived();
return getHandle(id, input);
}
return { start, getHandle };
}

View File

@@ -11,7 +11,7 @@ type CommandLine = {
id: string;
input: string;
output: string;
phase?: "running" | "finished";
phase?: "running" | "waiting" | "finished";
success?: boolean;
dimOutput?: boolean;
preformatted?: boolean;
@@ -30,6 +30,9 @@ type CommandLine = {
export const CommandMessage = memo(({ line }: { line: CommandLine }) => {
const columns = useTerminalWidth();
const rightWidth = Math.max(0, columns - 2); // gutter is 2 cols
if (line.phase === "waiting") {
return null;
}
// Determine dot state based on phase and success
const getDotElement = () => {

View File

@@ -88,7 +88,7 @@ const _colors = {
selected: brandColors.primaryAccent,
inactive: brandColors.textDisabled, // uses dimColor prop
border: brandColors.textDisabled,
running: brandColors.statusWarning,
running: brandColors.textSecondary,
error: brandColors.statusError,
},

View File

@@ -158,7 +158,7 @@ export type Line =
id: string;
input: string;
output: string;
phase?: "running" | "finished";
phase?: "running" | "waiting" | "finished";
success?: boolean;
dimOutput?: boolean;
preformatted?: boolean;

View File

@@ -0,0 +1,156 @@
import { describe, expect, test } from "bun:test";
import {
handleMcpUsage,
setActiveCommandId as setActiveMcpCommandId,
} from "../../cli/commands/mcp";
import {
addCommandResult,
setActiveCommandId as setActiveProfileCommandId,
updateCommandResult,
} from "../../cli/commands/profile";
import { createCommandRunner } from "../../cli/commands/runner";
import { createBuffers } from "../../cli/helpers/accumulator";
describe("commandRunner", () => {
test("start/finish writes a single command line", () => {
const buffers = createBuffers();
const buffersRef = { current: buffers };
let refreshCount = 0;
const runner = createCommandRunner({
buffersRef,
refreshDerived: () => {
refreshCount += 1;
},
createId: () => "cmd-1",
});
const cmd = runner.start("/model", "Opening model selector...");
expect(cmd.id).toBe("cmd-1");
expect(buffers.order).toEqual(["cmd-1"]);
expect(buffers.byId.get("cmd-1")).toMatchObject({
kind: "command",
input: "/model",
output: "Opening model selector...",
phase: "running",
});
cmd.finish("Done", true);
expect(buffers.byId.get("cmd-1")).toMatchObject({
kind: "command",
input: "/model",
output: "Done",
phase: "finished",
success: true,
});
expect(refreshCount).toBeGreaterThan(0);
});
test("getHandle preserves existing input and order", () => {
const buffers = createBuffers();
const buffersRef = { current: buffers };
buffers.byId.set("cmd-1", {
kind: "command",
id: "cmd-1",
input: "/connect",
output: "Starting...",
phase: "running",
});
buffers.order.push("cmd-1");
const runner = createCommandRunner({
buffersRef,
refreshDerived: () => {},
createId: () => "cmd-ignored",
});
const cmd = runner.getHandle("cmd-1", "/connect codex");
cmd.update({ output: "Still running...", phase: "running" });
const line = buffers.byId.get("cmd-1");
expect(line).toMatchObject({
kind: "command",
input: "/connect",
output: "Still running...",
phase: "running",
});
expect(buffers.order).toEqual(["cmd-1"]);
});
});
describe("command input preservation in handlers", () => {
test("mcp usage keeps original input when reusing command id", () => {
const buffers = createBuffers();
const buffersRef = { current: buffers };
buffers.byId.set("cmd-1", {
kind: "command",
id: "cmd-1",
input: "/mcp",
output: "",
phase: "running",
});
buffers.order.push("cmd-1");
setActiveMcpCommandId("cmd-1");
handleMcpUsage(
{
buffersRef,
refreshDerived: () => {},
setCommandRunning: () => {},
},
"/mcp add",
);
setActiveMcpCommandId(null);
const line = buffers.byId.get("cmd-1");
expect(line).toMatchObject({
kind: "command",
input: "/mcp",
});
expect(line?.kind).toBe("command");
if (line && line.kind === "command") {
expect(line.output).toContain("Usage: /mcp");
}
});
test("profile updates keep original input when reusing command id", () => {
const buffers = createBuffers();
const buffersRef = { current: buffers };
buffers.byId.set("cmd-1", {
kind: "command",
id: "cmd-1",
input: "/profile",
output: "",
phase: "running",
});
buffers.order.push("cmd-1");
setActiveProfileCommandId("cmd-1");
addCommandResult(
buffersRef,
() => {},
"/profile save test",
"Saving...",
false,
"running",
);
updateCommandResult(
buffersRef,
() => {},
"cmd-1",
"/profile delete test",
"Done",
true,
"finished",
);
setActiveProfileCommandId(null);
const line = buffers.byId.get("cmd-1");
expect(line).toMatchObject({
kind: "command",
input: "/profile",
output: "Done",
phase: "finished",
success: true,
});
});
});