feat(cli): add interactive cd command

This commit is contained in:
Cline Agent
2026-10-20 08:49:08 +09:00
committed by Dominic Cooney
parent 241c93026d
commit c450cff2e1
19 changed files with 417 additions and 28 deletions
+1 -1
View File
@@ -215,7 +215,7 @@ cline connect --stop
cline connect --stop telegram
```
In chat surfaces, connector slash commands include `/help`, `/start`, `/new`, `/clear`, `/whereami`, `/tools`, `/yolo`, `/cwd <path>`, `/schedule`, `/abort`, and `/exit`. Run `cline connect <adapter> --help` to see the full flag list for any adapter.
In chat surfaces, connector slash commands include `/help`, `/start`, `/new`, `/clear`, `/whereami`, `/tools`, `/yolo`, `/cd <path>` (also `/cwd <path>`), `/schedule`, `/abort`, and `/exit`. Run `cline connect <adapter> --help` to see the full flag list for any adapter.
### Schedules
+1 -1
View File
@@ -57,7 +57,7 @@ The Telegram connector uses the shared connector command parser:
- `/whereami` - show thread, cwd, tools, and yolo state
- `/tools [on|off|toggle]` - allow or block repo/file/shell tools
- `/yolo [on|off|toggle]` - auto-approve tool use
- `/cwd <path>` - change working directory
- `/cd <path>` or `/cwd <path>` - change working directory
- `/schedule create/list/trigger/delete` - manage scheduled workflows
- `/abort` - stop the current task
- `/exit` - stop the connector
+1 -1
View File
@@ -1026,7 +1026,7 @@ export async function handleConnectorUserTurn<
const { prompt, userImages, userFiles } = await buildUserInputMessage(
runtimeInput,
input.userInstructionService,
{ mode: startRequest.mode },
{ mode: startRequest.mode, cwd: startRequest.cwd },
);
try {
await input.client.sendRuntimeSession(
+1
View File
@@ -1210,6 +1210,7 @@ export async function runCli(): Promise<void> {
await runInteractive(config, userInstructionService, resumeSessionId, {
initialPrompt: args.prompt,
clineApiBaseUrl: initialClineProviderSettings?.baseUrl,
explicitSystemPrompt: args.systemPrompt,
clineProviderSettings: initialClineProviderSettings,
startupTarget,
initialNotice,
@@ -45,6 +45,7 @@ function makeRuntime(): InteractiveChatCommandRuntime {
getActiveSessionId: vi.fn(() => "session-1"),
resetForNewSession: vi.fn(async () => {}),
restartEmpty: vi.fn(async () => {}),
changeWorkingDirectory: vi.fn(async () => {}),
};
}
@@ -164,6 +165,41 @@ describe("runInteractiveChatCommand", () => {
expect(setInteractiveAutoApprove).toHaveBeenCalledWith(true);
});
it("changes the runtime working directory before reporting /cd success", async () => {
const config = makeConfig();
const state = makeState(config);
const runtime = makeRuntime();
const target = process.cwd();
state.cwd = "/tmp";
state.workspaceRoot = "/tmp";
vi.mocked(runtime.changeWorkingDirectory).mockImplementation(
async (next) => {
Object.assign(state, next);
},
);
const result = await runInteractiveChatCommand({
prompt: `/cd ${target}`,
enabled: true,
config,
host: chatCommandHost,
chatCommandState: state,
autoApproveAllRef: { current: false },
setInteractiveAutoApprove: () => {},
sessionRuntime: runtime,
stop: () => {},
});
expect(runtime.changeWorkingDirectory).toHaveBeenCalledWith(
expect.objectContaining({ cwd: target }),
);
expect(state.cwd).toBe(target);
expect(result).toMatchObject({
handled: true,
turnResult: { commandOutput: expect.stringContaining(`cwd=${target}`) },
});
});
it("returns plugin command submit prompts as model input", async () => {
const config = makeConfig();
const runtime = makeRuntime();
@@ -22,6 +22,7 @@ export type InteractiveChatCommandRuntime = Pick<
| "getActiveSessionId"
| "resetForNewSession"
| "restartEmpty"
| "changeWorkingDirectory"
>;
export type InteractiveChatCommandResult =
@@ -75,10 +76,14 @@ export async function runInteractiveChatCommand(input: {
autoApproveTools: input.autoApproveAllRef.current,
}),
setState: async (next) => {
input.chatCommandState.enableTools = next.enableTools;
input.chatCommandState.autoApproveTools = next.autoApproveTools;
input.chatCommandState.cwd = next.cwd;
input.chatCommandState.workspaceRoot = next.workspaceRoot;
if (
next.cwd !== input.chatCommandState.cwd ||
next.workspaceRoot !== input.chatCommandState.workspaceRoot
) {
await input.sessionRuntime.changeWorkingDirectory(next);
} else {
Object.assign(input.chatCommandState, next);
}
input.setInteractiveAutoApprove(next.autoApproveTools);
},
reply: async (text) => {
@@ -22,6 +22,7 @@ const subscribeToPendingPromptEventsMock = vi.hoisted(() => vi.fn());
const markAbortInProgressMock = vi.hoisted(() => vi.fn());
const submitAndExitInTerminalMock = vi.hoisted(() => vi.fn());
const createInteractiveExitSummaryMock = vi.hoisted(() => vi.fn());
const resolveSystemPromptMock = vi.hoisted(() => vi.fn());
vi.mock("../../session/session", () => ({
createCliCore: createCliCoreMock,
@@ -47,6 +48,10 @@ vi.mock("../active-runtime", () => ({
markAbortInProgress: markAbortInProgressMock,
}));
vi.mock("../prompt", () => ({
resolveSystemPrompt: resolveSystemPromptMock,
}));
vi.mock("../session-events", () => ({
subscribeToAgentEvents: subscribeToAgentEventsMock,
subscribeToPendingPromptEvents: subscribeToPendingPromptEventsMock,
@@ -233,10 +238,12 @@ describe("createInteractiveSessionRuntime", () => {
markAbortInProgressMock.mockReset();
submitAndExitInTerminalMock.mockReset();
createInteractiveExitSummaryMock.mockReset();
resolveSystemPromptMock.mockReset();
createRuntimeHooksMock.mockReturnValue({
hooks: undefined,
shutdown: vi.fn().mockResolvedValue(undefined),
});
resolveSystemPromptMock.mockResolvedValue("rebuilt system prompt");
loadInteractiveResumeMessagesMock.mockResolvedValue([]);
subscribeToAgentEventsMock.mockReturnValue(() => {});
subscribeToPendingPromptEventsMock.mockReturnValue(() => {});
@@ -587,6 +594,107 @@ describe("createInteractiveSessionRuntime", () => {
expect(runtime.getActiveSessionId()).toBe("session-restarted");
});
it("restarts the active session with one working-directory snapshot", async () => {
const manager = makeManager();
const config = createConfig();
const state = createChatCommandState(config);
createCliCoreMock.mockResolvedValue(manager);
const { createInteractiveSessionRuntime } = await importRuntime();
const runtime = createInteractiveSessionRuntime({
config,
providerSettingsManager: createProviderSettingsManager(),
explicitSystemPrompt: "custom prompt",
chatCommandState: state,
requestToolApproval: vi.fn(),
resolveToolPolicy: () => ({ autoApprove: true }),
askQuestionRef: { current: null },
resolveMistakeLimitDecision: undefined,
switchToActModeTool: {} as never,
onAgentEvent: vi.fn(),
onTeamEvent: vi.fn(),
onPendingPrompts: vi.fn(),
onPendingPromptSubmitted: vi.fn(),
});
await runtime.ensureReady();
await runtime.changeWorkingDirectory({
...state,
cwd: "/tmp/next-project",
workspaceRoot: "/tmp/next-project",
});
expect(resolveSystemPromptMock).toHaveBeenCalledWith({
cwd: "/tmp/next-project",
explicitSystemPrompt: "custom prompt",
providerId: "anthropic",
mode: "act",
});
expect(config).toMatchObject({
cwd: "/tmp/next-project",
workspaceRoot: "/tmp/next-project",
systemPrompt: "rebuilt system prompt",
});
expect(state).toMatchObject({
cwd: "/tmp/next-project",
workspaceRoot: "/tmp/next-project",
});
expect(manager.stop).toHaveBeenCalledWith("session-1");
expect(manager.start).toHaveBeenCalledTimes(2);
expect(manager.start.mock.calls[1]?.[0]).toEqual(
expect.objectContaining({
config: expect.objectContaining({
cwd: "/tmp/next-project",
workspaceRoot: "/tmp/next-project",
systemPrompt: "rebuilt system prompt",
}),
}),
);
expect(createRuntimeHooksMock).toHaveBeenLastCalledWith(
expect.objectContaining({
cwd: "/tmp/next-project",
workspaceRoot: "/tmp/next-project",
}),
);
});
it("restores the previous working-directory snapshot when restart fails", async () => {
const manager = makeManager();
const config = createConfig();
const state = createChatCommandState(config);
const runtime = await makeRuntime(manager, { config });
await runtime.ensureReady();
manager.start.mockRejectedValueOnce(new Error("replacement failed"));
await expect(
runtime.changeWorkingDirectory({
...state,
cwd: "/tmp/failed-project",
workspaceRoot: "/tmp/failed-project",
}),
).rejects.toThrow("replacement failed");
expect(config).toMatchObject({
cwd: "/tmp/project",
workspaceRoot: "/tmp/project",
systemPrompt: "system",
});
expect(state).toMatchObject({
cwd: "/tmp/project",
workspaceRoot: "/tmp/project",
});
expect(manager.start).toHaveBeenCalledTimes(3);
expect(manager.start.mock.calls[2]?.[0]).toEqual(
expect.objectContaining({
config: expect.objectContaining({
sessionId: "session-1",
cwd: "/tmp/project",
workspaceRoot: "/tmp/project",
}),
}),
);
expect(runtime.getActiveSessionId()).toBe("session-2");
});
it("adds a live interactive approval policy hook to started sessions", async () => {
const manager = makeManager();
const upstreamBeforeTool = vi.fn(async () => ({
@@ -1,3 +1,4 @@
import { basename } from "node:path";
import {
type AgentEvent,
type AgentHooks,
@@ -32,6 +33,7 @@ import { formatPluginIssueWarnings } from "../../utils/plugin-issues";
import { loadInteractiveResumeMessages } from "../../utils/resume";
import type { Config } from "../../utils/types";
import { markAbortInProgress } from "../active-runtime";
import { resolveSystemPrompt } from "../prompt";
import type {
PendingPromptSnapshot,
PendingPromptSubmittedEvent,
@@ -98,6 +100,7 @@ export function createInteractiveSessionRuntime(input: {
config: Config;
providerSettingsManager: ProviderSettingsManager;
userInstructionService?: UserInstructionConfigService;
explicitSystemPrompt?: string;
resumeSessionId?: string;
chatCommandState: ChatCommandState;
requestToolApproval: (
@@ -131,6 +134,20 @@ export function createInteractiveSessionRuntime(input: {
let pendingResumeSessionId = input.resumeSessionId?.trim() || undefined;
const createWorkspaceRuntimeHooks = (
manager: CliCore,
workspace: Pick<ChatCommandState, "cwd" | "workspaceRoot">,
): RuntimeHooks =>
createRuntimeHooks({
verbose: input.config.verbose,
yolo: input.config.mode === "yolo",
cwd: workspace.cwd,
workspaceRoot: workspace.workspaceRoot,
dispatchHookEvent: async (payload) => {
await manager.ingestHookEvent(payload);
},
});
const clearActiveSession = (): void => {
activeSessionId = "";
setActiveCliSession(undefined);
@@ -188,15 +205,7 @@ export function createInteractiveSessionRuntime(input: {
throw new Error("interactive runtime shutdown requested");
}
sessionManager = manager;
runtimeHooks = createRuntimeHooks({
verbose: input.config.verbose,
yolo: input.config.mode === "yolo",
cwd: input.config.cwd,
workspaceRoot: input.config.workspaceRoot,
dispatchHookEvent: async (payload) => {
await manager.ingestHookEvent(payload);
},
});
runtimeHooks = createWorkspaceRuntimeHooks(manager, input.chatCommandState);
unsubscribeAgent = subscribeToAgentEvents(manager, input.onAgentEvent);
unsubscribePendingPrompts = subscribeToPendingPromptEvents(manager, {
onPendingPrompts: input.onPendingPrompts,
@@ -450,14 +459,14 @@ export function createInteractiveSessionRuntime(input: {
messages: MessageWithMetadata[],
sessionMetadata?: Record<string, unknown>,
initialCompactionState?: SessionCompactionState,
options?: { preserveSessionId?: boolean },
options?: { preserveSessionId?: boolean; sessionId?: string },
): Promise<void> => {
// Config-only restarts (model/mode/account changes) continue the same
// conversation, so they must keep the session id — otherwise each
// restart mints a new session history entry for the same conversation.
const reuseSessionId = options?.preserveSessionId
? activeSessionId || undefined
: undefined;
const reuseSessionId =
options?.sessionId ??
(options?.preserveSessionId ? activeSessionId || undefined : undefined);
sessionStartGeneration += 1;
pendingResumeSessionId = undefined;
startupError = undefined;
@@ -520,6 +529,99 @@ export function createInteractiveSessionRuntime(input: {
);
};
const changeWorkingDirectory = async (
next: ChatCommandState,
): Promise<void> => {
await ensureReady();
const manager = sessionManager;
if (!manager) {
throw new Error("interactive session manager is unavailable");
}
const sourceSessionId = activeSessionId;
const [{ messages, status }, compactionState, systemPrompt] =
await Promise.all([
readCurrentMessages(),
readCurrentCompactionState(),
resolveSystemPrompt({
cwd: next.cwd,
explicitSystemPrompt: input.explicitSystemPrompt,
providerId: input.config.providerId,
mode: input.config.mode,
}),
]);
if (status !== "read" || activeSessionId !== sourceSessionId) {
throw new Error("Working directory changed concurrently. Try /cd again.");
}
const previousState = { ...input.chatCommandState };
const previousSessionId = activeSessionId;
const previousConfig = {
cwd: input.config.cwd,
workspaceRoot: input.config.workspaceRoot,
systemPrompt: input.config.systemPrompt,
extensionContext: input.config.extensionContext,
};
const previousRuntimeHooks = runtimeHooks;
const nextRuntimeHooks = createWorkspaceRuntimeHooks(manager, next);
const projectedMessages = compactionState
? projectSessionCompactionState(compactionState, messages)
: undefined;
const initialCompactionState = projectedMessages
? createSessionCompactionState({
sourceMessages: messages,
compactedMessages: projectedMessages,
systemPrompt: compactionState?.system_prompt,
})
: undefined;
// The directory becomes effective as one snapshot for the replacement
// session. A concurrent ensureReady() waits on the restart barrier.
Object.assign(input.chatCommandState, next);
input.config.cwd = next.cwd;
input.config.workspaceRoot = next.workspaceRoot;
input.config.systemPrompt = systemPrompt;
if (input.config.extensionContext?.workspace) {
input.config.extensionContext = {
...input.config.extensionContext,
workspace: {
...input.config.extensionContext.workspace,
rootPath: next.workspaceRoot,
cwd: next.cwd,
workspaceName: basename(next.cwd),
},
};
}
runtimeHooks = nextRuntimeHooks;
try {
await restartWithMessages(messages, undefined, initialCompactionState, {
preserveSessionId: true,
});
} catch (error) {
Object.assign(input.chatCommandState, previousState);
input.config.cwd = previousConfig.cwd;
input.config.workspaceRoot = previousConfig.workspaceRoot;
input.config.systemPrompt = previousConfig.systemPrompt;
input.config.extensionContext = previousConfig.extensionContext;
runtimeHooks = previousRuntimeHooks;
await nextRuntimeHooks.shutdown().catch(() => {});
try {
await restartWithMessages(messages, undefined, initialCompactionState, {
sessionId: previousSessionId || undefined,
});
} catch (recoveryError) {
throw new AggregateError(
[error, recoveryError],
"Working directory change failed, and the previous session could not be restored.",
);
}
throw error;
}
await previousRuntimeHooks?.shutdown().catch(() => {});
};
const updateCurrentSessionConnection = async (
update: SessionConnectionUpdate,
): Promise<void> => {
@@ -919,6 +1021,7 @@ export function createInteractiveSessionRuntime(input: {
resetForNewSession,
restartWithMessages,
restartWithCurrentMessages,
changeWorkingDirectory,
updateCurrentSessionConnection,
resumeSession,
forkCurrentSession,
+15
View File
@@ -51,6 +51,21 @@ describe("buildUserInputMessage", () => {
expect(result.userImages).toEqual([]);
expect(result.userFiles).toEqual([filePath]);
});
it("resolves relative file mentions from the configured working directory", async () => {
const dir = mkdtempSync(join(tmpdir(), "cli-prompt-cwd-"));
const filePath = join(dir, "notes.md");
writeFileSync(filePath, "# Notes\n");
const result = await buildUserInputMessage(
"summarize @./notes.md",
undefined,
{ cwd: dir },
);
expect(result.prompt).toBe("summarize [file: notes.md]");
expect(result.userFiles).toEqual([filePath]);
});
});
describe("resolveSystemPrompt workspace metadata", () => {
+7 -4
View File
@@ -74,11 +74,11 @@ function extractFileMentions(
return matches;
}
function resolveMentionPath(filePath: string): string {
function resolveMentionPath(filePath: string, cwd: string): string {
if (filePath.startsWith("~/")) {
return resolve(homedir(), filePath.slice(2));
}
return resolve(filePath);
return resolve(cwd, filePath);
}
/**
@@ -104,7 +104,7 @@ export function shouldExpandSkillSlashCommands(mode?: string): boolean {
export async function buildUserInputMessage(
rawPrompt: string,
userInstructionService?: UserInstructionConfigService,
options?: { mode?: string },
options?: { mode?: string; cwd?: string },
): Promise<{
prompt: string;
userImages: string[];
@@ -154,7 +154,10 @@ export async function buildUserInputMessage(
for (const mention of fileMentions) {
try {
const resolvedPath = resolveMentionPath(mention.path);
const resolvedPath = resolveMentionPath(
mention.path,
options?.cwd ?? process.cwd(),
);
const stats = statSync(resolvedPath);
if (!stats.isFile()) {
throw new Error(`Path is not a file: ${resolvedPath}`);
+1
View File
@@ -281,6 +281,7 @@ export async function runAgent(
userFiles,
} = await buildUserInputMessage(prompt, userInstructionService, {
mode: config.mode,
cwd: config.cwd,
});
const started = await sessionManager.start({
source: SessionSource.CLI,
+3
View File
@@ -186,6 +186,7 @@ export async function runInteractive(
initialPrompt?: string;
initialNotice?: CliMigrationNotice;
onInitialNoticeShown?: (notice: CliMigrationNotice) => void | Promise<void>;
explicitSystemPrompt?: string;
},
): Promise<void> {
assertInteractivePreflight(config);
@@ -287,6 +288,7 @@ export async function runInteractive(
config,
providerSettingsManager,
userInstructionService,
explicitSystemPrompt: options?.explicitSystemPrompt,
resumeSessionId,
chatCommandState,
requestToolApproval,
@@ -634,6 +636,7 @@ export async function runInteractive(
userFiles,
} = await buildUserInputMessage(input, userInstructionService, {
mode,
cwd: config.cwd,
});
const mergedUserImages = [
...(attachments?.userImages ?? []),
+1
View File
@@ -82,6 +82,7 @@ export async function runZen(
// Zen runs in yolo mode, whose preset has no skills tool — skill
// commands must keep expanding textually.
mode: "yolo",
cwd: config.cwd,
});
const startRequest: ChatStartSessionRequest = {
@@ -316,4 +316,26 @@ describe("slash command registry", () => {
getVisibleSystemSlashCommands(registry).map((command) => command.name),
).toContain("account");
});
it("exposes cd as a runtime command", () => {
const registry = buildSlashCommandRegistry({
workflowSlashCommands: [
{
name: "cd",
instructions: "/cd <directory>",
description: "Change the working directory",
},
],
});
expect(resolveSlashCommand(registry, "cd")).toMatchObject({
source: "runtime",
execution: "runtime",
visible: true,
selectable: true,
});
expect(
getVisibleSystemSlashCommands(registry).map((command) => command.name),
).toContain("cd");
});
});
@@ -116,6 +116,7 @@ const TUI_LOCAL_COMMANDS: Array<{
const SYSTEM_COMMAND_ORDER = [
"settings",
"cd",
"model",
"theme",
"account",
@@ -130,7 +131,7 @@ const SYSTEM_COMMAND_ORDER = [
"history",
"help",
"quit",
] satisfies ReadonlyArray<LocalSlashCommandName | "team">;
] satisfies ReadonlyArray<LocalSlashCommandName | "cd" | "team">;
const SYSTEM_COMMAND_PRIORITY = new Map<string, number>(
SYSTEM_COMMAND_ORDER.map((name, index) => [name, index]),
@@ -133,6 +133,12 @@ const HELP_ROWS: HelpRow[] = [
key: "/theme",
desc: "Change color theme",
},
{
kind: "entry",
id: "c-cd",
key: "/cd <directory>",
desc: "Change the working directory",
},
{
kind: "entry",
id: "c-mcp",
+5
View File
@@ -123,6 +123,11 @@ export function listInteractiveSlashCommands(
instructions: "",
description: "Modify agent configuration",
},
{
name: "cd",
instructions: "/cd <directory>",
description: "Change the working directory",
},
{
name: "mcp",
instructions: "",
+62
View File
@@ -1,3 +1,6 @@
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
createChatCommandHost,
@@ -199,6 +202,65 @@ describe("chat commands", () => {
expect(reply).toHaveBeenCalledWith("hello world");
});
it("changes directories with both /cd and the existing /cwd spelling", async () => {
const root = mkdtempSync(join(tmpdir(), "cli-chat-cd-"));
const target = join(root, "project with spaces");
mkdirSync(target);
for (const command of [
`/cd "project with spaces"`,
`/cwd "project with spaces"`,
]) {
const state = {
enableTools: true,
autoApproveTools: false,
cwd: root,
workspaceRoot: root,
};
const setState = vi.fn(async (next) => Object.assign(state, next));
const reply = vi.fn(async () => undefined);
expect(
await maybeHandleChatCommand(command, {
enabled: true,
getState: () => state,
setState,
reply,
}),
).toBe(true);
expect(state.cwd).toBe(target);
expect(setState).toHaveBeenCalledOnce();
expect(reply).toHaveBeenCalledWith(
expect.stringContaining(`cwd=${target}`),
);
}
});
it("leaves the working directory unchanged when /cd is not a directory", async () => {
const root = mkdtempSync(join(tmpdir(), "cli-chat-cd-invalid-"));
writeFileSync(join(root, "file.txt"), "not a directory");
const setState = vi.fn(async () => undefined);
const reply = vi.fn(async () => undefined);
expect(
await maybeHandleChatCommand("/cd file.txt", {
enabled: true,
getState: () => ({
enableTools: true,
autoApproveTools: false,
cwd: root,
workspaceRoot: root,
}),
setState,
reply,
}),
).toBe(true);
expect(setState).not.toHaveBeenCalled();
expect(reply).toHaveBeenCalledWith(
`invalid directory: ${join(root, "file.txt")}`,
);
});
it("shows usage for /team with no arguments", async () => {
const reply = vi.fn(async () => undefined);
+20 -3
View File
@@ -1,4 +1,5 @@
import { stat } from "node:fs/promises";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { resolveWorkspaceRoot } from "./helpers";
@@ -216,6 +217,22 @@ function tokenizeArgs(input: string): string[] {
return tokens;
}
function resolveChatCommandDirectory(cwd: string, args: string[]): string {
const rawPath = args.join(" ").trim();
const unquotedPath =
(rawPath.startsWith('"') && rawPath.endsWith('"')) ||
(rawPath.startsWith("'") && rawPath.endsWith("'"))
? rawPath.slice(1, -1)
: rawPath;
if (unquotedPath === "~") {
return homedir();
}
if (unquotedPath.startsWith("~/")) {
return resolve(homedir(), unquotedPath.slice(2));
}
return resolve(cwd, unquotedPath);
}
function parseFlagValues(tokens: string[]): {
positionals: string[];
flags: Record<string, string>;
@@ -282,7 +299,7 @@ function formatHelp(state: ChatCommandState): string {
"/whereami - show thread, cwd, tools, and yolo state",
"/tools [on|off|toggle] - allow repo/file/shell tools",
"/yolo [on|off|toggle] - auto-approve tool use",
"/cwd <path> - change working directory",
"/cd <path> (or /cwd <path>) - change working directory",
"/schedule create/list/trigger/delete - manage scheduled workflows",
"/abort - stop the current task",
"/mute [target] - ignore this thread or target until /unmute",
@@ -398,7 +415,7 @@ function createDefaultChatCommandHost(): ChatCommandHost {
},
})
.register("command", {
names: ["/cwd"],
names: ["/cd", "/cwd"],
run: async ({ args, state }, context) => {
const rawPath = args.join(" ").trim();
if (!rawPath) {
@@ -407,7 +424,7 @@ function createDefaultChatCommandHost(): ChatCommandHost {
);
return;
}
const nextCwd = resolve(state.cwd, rawPath);
const nextCwd = resolveChatCommandDirectory(state.cwd, args);
const fileStat = await stat(nextCwd).catch(() => undefined);
if (!fileStat?.isDirectory()) {
await context.reply(`invalid directory: ${nextCwd}`);