Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion src/cli/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ import {
isDebugEnabled,
} from "../utils/debug";
import { getVersion } from "../version";
import {
buildCavemanCommandPrompt,
CAVEMAN_MODE_HINT,
isCavemanCommandInput,
normalizeCavemanMode,
suppressPreparedClientTools,
} from "./commands/caveman";
import {
handleMcpAdd,
type McpCommandContext,
Expand Down Expand Up @@ -4002,10 +4009,12 @@ export default function App({
initialInput: Array<MessageCreate | ApprovalCreate>,
options?: {
allowReentry?: boolean;
suppressClientTools?: boolean;
submissionGeneration?: number;
transcriptStartLineIndex?: number | null;
},
): Promise<void> => {
const suppressClientTools = options?.suppressClientTools ?? false;
// Transient pre-stream retries can yield for seconds.
// Pin the user's permission mode for the duration of the submission so
// auto-approvals (YOLO / bypassPermissions) don't regress after a retry.
Expand Down Expand Up @@ -4350,13 +4359,18 @@ export default function App({
const preparedToolContext = await prepareScopedToolExecutionContext(
tempModelOverrideRef.current ?? undefined,
);
const preparedToolContextForRequest = suppressClientTools
? suppressPreparedClientTools(
preparedToolContext.preparedToolContext,
)
: preparedToolContext.preparedToolContext;
const nextStream = await sendMessageStream(
conversationIdRef.current,
currentInput,
{
agentId: agentIdRef.current,
overrideModel: tempModelOverrideRef.current ?? undefined,
preparedToolContext: preparedToolContext.preparedToolContext,
preparedToolContext: preparedToolContextForRequest,
},
);
stream = nextStream;
Expand Down Expand Up @@ -10483,6 +10497,63 @@ export default function App({
return { submitted: true };
}

// /caveman - switch cave-code response/thinking mode
if (isCavemanCommandInput(trimmed)) {
const modeInput = trimmed.slice("/caveman".length).trim();
const mode = normalizeCavemanMode(modeInput);

if (!mode) {
addCommandResult(
buffersRef,
refreshDerived,
msg,
`Usage: /caveman ${CAVEMAN_MODE_HINT}`,
false,
);
return { submitted: true };
}

const cmd = commandRunner.start(
msg,
`Switching cave-code to ${mode} mode...`,
);

const approvalCheck = await checkPendingApprovalsForSlashCommand();
if (approvalCheck.blocked) {
cmd.fail(
"Pending approval(s). Resolve approvals before running /caveman.",
);
return { submitted: false };
}

setCommandRunning(true);

try {
const prompt = buildCavemanCommandPrompt(mode);
cmd.finish(`Switching cave-code to ${mode} mode...`, true);
await processConversation(
[
{
type: "message",
role: "user",
content: buildTextParts(
`${SYSTEM_REMINDER_OPEN}\n${prompt}\n${SYSTEM_REMINDER_CLOSE}`,
),
otid: randomUUID(),
},
],
{ suppressClientTools: true },
);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(`Failed: ${errorDetails}`);
} finally {
setCommandRunning(false);
}

return { submitted: true };
}

// Special handling for /remember command - remember something from conversation
if (trimmed.startsWith("/remember")) {
// Extract optional description after `/remember`
Expand Down
86 changes: 86 additions & 0 deletions src/cli/commands/caveman.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { PreparedToolExecutionContext } from "../../tools/manager";

export const CAVEMAN_MODE_HINT =
"[lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra]";

export const CAVEMAN_MODES = [
"lite",
"full",
"ultra",
"wenyan-lite",
"wenyan-full",
"wenyan-ultra",
] as const;

export type CavemanMode = (typeof CAVEMAN_MODES)[number];

const CAVEMAN_COMMAND_PATTERN = /^\/caveman(?:\s|$)/;

const CAVEMAN_MODE_ALIASES: Record<string, CavemanMode> = {
"": "full",
lite: "lite",
full: "full",
ultra: "ultra",
ulta: "ultra",
wenyan: "wenyan-full",
"wenyan-lite": "wenyan-lite",
"wenyan-full": "wenyan-full",
"wenyan-ultra": "wenyan-ultra",
"wenyan-ulta": "wenyan-ultra",
};

export const CAVEMAN_MODE_RULES: Record<CavemanMode, string[]> = {
lite: [
"Mode rules: remove filler, pleasantries, and hedging, but keep articles and complete professional sentences.",
"Example style: Component re-renders because object reference changes each render. Wrap it in `useMemo`.",
],
full: [
"Mode rules: drop articles, fragments are okay, use short synonyms, and keep classic cave-code compression.",
"Example style: New object ref each render. Inline prop = new ref = re-render. Wrap in `useMemo`.",
],
ultra: [
"Mode rules: abbreviate common technical nouns, strip conjunctions, use arrows for causality, and use one word when enough.",
"Example style: Inline obj prop -> new ref -> re-render. `useMemo`.",
],
"wenyan-lite": [
"Mode rules: use semi-classical Chinese register, drop filler and hedging, but keep readable grammar structure.",
"Example style: 組件頻重繪,以每繪新生對象參照故。以 `useMemo` 包之。",
],
"wenyan-full": [
"Mode rules: write compact 文言文: major character reduction, subject omission, verb-object terseness, particles like 之/乃/為/其.",
"Example style: 物出新參照,致重繪。`useMemo` 包之。",
],
"wenyan-ultra": [
"Mode rules: extreme compact 文言 style, maximum compression, arrows allowed when they clarify cause.",
"Example style: 新參照->重繪。`useMemo`。",
],
};

export function isCavemanCommandInput(input: string): boolean {
return CAVEMAN_COMMAND_PATTERN.test(input);
}

export function normalizeCavemanMode(input: string): CavemanMode | null {
const normalized = input.trim().toLowerCase();
return CAVEMAN_MODE_ALIASES[normalized] ?? null;
}

export function buildCavemanCommandPrompt(mode: CavemanMode): string {
return [
`Switch to cave-code ${mode} mode.`,
...CAVEMAN_MODE_RULES[mode],
"Apply this mode for this conversation only. Do not call any tools for this mode switch.",
"Hidden reasoning, plans, and visible replies all follow the selected cave-code mode.",
"Technical terms stay exact. Code and quoted errors stay unchanged.",
"If safety-critical, destructive, or easy to misunderstand, switch to clear normal language for that part, then return to cave-code.",
].join("\n");
}

export function suppressPreparedClientTools(
preparedToolContext: PreparedToolExecutionContext,
): PreparedToolExecutionContext {
return {
...preparedToolContext,
clientTools: [],
};
}
31 changes: 28 additions & 3 deletions src/cli/commands/registry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// src/cli/commands/registry.ts
// Registry of available CLI commands

import { CAVEMAN_MODE_HINT, normalizeCavemanMode } from "./caveman";
import { handleSecretCommand } from "./secret";

type CommandHandler = (args: string[]) => Promise<string> | string;
type CommandResult = { success: boolean; output: string };
type CommandHandler = (
args: string[],
) => Promise<string | CommandResult> | string | CommandResult;

interface Command {
desc: string;
Expand Down Expand Up @@ -93,6 +97,24 @@ export const commands: Record<string, Command> = {
return "Starting skill creation...";
},
},
"/caveman": {
desc: "Switch cave-code mode",
args: CAVEMAN_MODE_HINT,
order: 29,
handler: (args) => {
const mode = normalizeCavemanMode(args.join(" "));
if (!mode) {
return {
success: false,
output: `Usage: /caveman ${CAVEMAN_MODE_HINT}`,
};
}
return {
success: false,
output: `/caveman ${mode} must be used inside the interactive CLI; mode was not applied.`,
};
},
},
"/memory": {
desc: "View your agent's memory",
order: 15,
Expand Down Expand Up @@ -620,8 +642,11 @@ export async function executeCommand(
}

try {
const output = await handler.handler(args);
return { success: true, output };
const result = await handler.handler(args);
if (typeof result === "string") {
return { success: true, output: result };
}
return result;
} catch (error) {
return {
success: false,
Expand Down
79 changes: 79 additions & 0 deletions src/tests/cli/caveman-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, test } from "bun:test";
import {
buildCavemanCommandPrompt,
isCavemanCommandInput,
normalizeCavemanMode,
suppressPreparedClientTools,
} from "../../cli/commands/caveman";
import { commands, executeCommand } from "../../cli/commands/registry";
import type { PreparedToolExecutionContext } from "../../tools/manager";

describe("/caveman command", () => {
test("matches slash-first caveman commands with trailing whitespace separators", () => {
expect(isCavemanCommandInput("/caveman")).toBe(true);
expect(isCavemanCommandInput("/caveman ultra")).toBe(true);
expect(isCavemanCommandInput("/caveman\tultra")).toBe(true);
expect(isCavemanCommandInput(" /caveman ultra")).toBe(false);
expect(isCavemanCommandInput("/cavemanultra")).toBe(false);
});

test("normalizes supported cave-code modes", () => {
expect(normalizeCavemanMode("")).toBe("full");
expect(normalizeCavemanMode("lite")).toBe("lite");
expect(normalizeCavemanMode("full")).toBe("full");
expect(normalizeCavemanMode("ultra")).toBe("ultra");
expect(normalizeCavemanMode("ulta")).toBe("ultra");
expect(normalizeCavemanMode("wenyan")).toBe("wenyan-full");
expect(normalizeCavemanMode("wenyan-lite")).toBe("wenyan-lite");
expect(normalizeCavemanMode("wenyan-full")).toBe("wenyan-full");
expect(normalizeCavemanMode("wenyan-ultra")).toBe("wenyan-ultra");
expect(normalizeCavemanMode("wenyan-ulta")).toBe("wenyan-ultra");
expect(normalizeCavemanMode("verbose")).toBeNull();
});

test("builds a mode-switch prompt without tool use", () => {
const prompt = buildCavemanCommandPrompt("ultra");

expect(prompt).toContain("Switch to cave-code ultra mode.");
expect(prompt).toContain("abbreviate common technical nouns");
expect(prompt).toContain("Do not call any tools");
expect(prompt).toContain("for this conversation only");
});

test("registers /caveman as a built-in slash command", async () => {
expect(commands["/caveman"]).toMatchObject({
desc: "Switch cave-code mode",
});

await expect(executeCommand("/caveman ultra")).resolves.toEqual({
success: false,
output:
"/caveman ultra must be used inside the interactive CLI; mode was not applied.",
});
await expect(executeCommand("/caveman nonsense")).resolves.toEqual({
success: false,
output:
"Usage: /caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra]",
});
});

test("suppresses advertised client tools while preserving execution context", () => {
const preparedToolContext: PreparedToolExecutionContext = {
contextId: "ctx-1",
loadedToolNames: ["Bash"],
clientTools: [
{
name: "Bash",
description: "Run a shell command",
parameters: { type: "object" },
},
],
};

expect(suppressPreparedClientTools(preparedToolContext)).toEqual({
contextId: "ctx-1",
loadedToolNames: ["Bash"],
clientTools: [],
});
});
});