anomalyco / anomalyco/opencode

[FEATURE]: Add /cd directory switching command and change_directory agent tool

Open
#43,223 5 comments 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Aug 18, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Feature hasn't been suggested before.
  • I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request

Add a /cd TUI slash command and a change_directory agent tool that lets users and agents switch the session working directory without restarting. This supports worktree-based workflows where you move between git worktrees of the same project.

TUI command (/cd): Opens a dialog, resolves relative paths against the current directory, calls moveSession, sends a synthetic system-reminder to the session, refreshes VCS state, and shows a toast.

Agent tool (change_directory): Resolves relative paths against the instance directory (not process.cwd()), calls MoveSession.Service.moveSession, and returns success/error output.

Both use the existing experimental.controlPlane.moveSession API.

Reference implementation — TUI command
// packages/tui/src/feature-plugins/home/change-directory.tsx
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
import type { BuiltinTuiPlugin } from "../builtins"
import { createSignal, Show } from "solid-js"
import { reconcile } from "solid-js/store"
import { useSDK } from "../../context/sdk"
import { useSync } from "../../context/sync"
import { useToast } from "../../ui/toast"
import { useDialog } from "../../ui/dialog"
import { errorMessage } from "../../util/error"
import { useProject } from "../../context/project"
import { useRoute } from "../../context/route"
import path from "path"

function ChangeDirectoryDialog(props: { api: TuiPluginApi }) {
  const sdk = useSDK()
  const sync = useSync()
  const toast = useToast()
  const dialog = useDialog()
  const project = useProject()
  const route = useRoute()
  const [input, setInput] = createSignal("")
  const [busy, setBusy] = createSignal(false)

  const currentDir = () => project.instance.directory() ?? ""

  async function submit() {
    const target = input().trim()
    if (!target) return
    const sessionID = route.data.type === "session" ? route.data.sessionID : undefined
    if (!sessionID) {
      toast.show({ title: "No active session", message: "Open a session before changing directory", variant: "error" })
      return
    }
    const resolved = path.isAbsolute(target) ? target : path.resolve(currentDir(), target)
    setBusy(true)
    try {
      await sdk.client.experimental.controlPlane.moveSession(
        { sessionID, destination: { directory: resolved }, moveChanges: false },
        { throwOnError: true },
      )
      // Non-critical: the move succeeded; a failed reminder doesn't warrant a user-facing error
      await sdk.client.session.promptAsync({
        sessionID, directory: resolved, noReply: true,
        parts: [{ type: "text", text: `<system-reminder>The user has changed the working directory to "${target}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.</system-reminder>`, synthetic: true }],
      }).catch(() => undefined)
      const res = await sdk.client.vcs.get({ workspace: project.workspace.current() })
      sync.set("vcs", reconcile(res.data))
      toast.show({ title: "Changed directory", message: resolved, variant: "success" })
      dialog.clear()
    } catch (err) {
      toast.show({ title: "Failed to change directory", message: errorMessage(err), variant: "error" })
    } finally {
      setBusy(false)
    }
  }

  return (
    <box flexDirection="column" gap={1} padding={1}>
      <text fg={props.api.theme.current.text}>Change working directory</text>
      <text fg={props.api.theme.current.textMuted}>Current: {currentDir()}</text>
      <box flexDirection="row" gap={1}>
        <text fg={props.api.theme.current.text}>{">"}</text>
        <input value={input()} onInput={(value) => setInput(value)} onSubmit={() => void submit()} placeholder="/path/to/worktree" />
      </box>
      <box flexDirection="row" gap={2}>
        <Show when={!busy()}><text fg={props.api.theme.current.accent}>Enter to confirm</text></Show>
        <Show when={busy()}><text fg={props.api.theme.current.textMuted}>Moving...</text></Show>
        <text fg={props.api.theme.current.textMuted}>Esc to cancel</text>
      </box>
    </box>
  )
}

const tui: TuiPlugin = async (api) => {
  api.keymap.registerLayer({
    commands: [{
      name: "cd.open", title: "Change directory", slashName: "cd",
      category: "Session", namespace: "palette",
      run() { api.ui.dialog.replace(() => <ChangeDirectoryDialog api={api} />) },
    }],
  })
}

const plugin: BuiltinTuiPlugin = { id: "change-directory", tui }
export default plugin
Reference implementation — Agent tool
// packages/opencode/src/tool/change-directory.ts
import path from "path"
import { Effect, Schema } from "effect"
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
import { SessionV2 } from "@opencode-ai/core/session"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Tool } from "./tool"
import { InstanceState } from "@/effect/instance-state"
import DESCRIPTION from "./change-directory.txt"

export const Parameters = Schema.Struct({
  directory: Schema.String.annotate({
    description: "The directory to switch to (absolute or relative to the current working directory)",
  }),
  moveChanges: Schema.optional(Schema.Boolean).annotate({
    description: "Whether to move uncommitted changes to the destination directory. Defaults to false.",
  }),
})

export const ChangeDirectoryTool = Tool.define<typeof Parameters, { directory: string }, MoveSession.Service>(
  "change_directory",
  Effect.gen(function* () {
    const moveSession = yield* MoveSession.Service
    return {
      description: DESCRIPTION,
      parameters: Parameters,
      execute: (params, ctx) =>
        Effect.gen(function* () {
          yield* ctx.ask({
            permission: "change_directory",
            patterns: [params.directory],
            always: [params.directory],
            metadata: { directory: params.directory },
          })
          const instance = yield* InstanceState.context
          const directory = AbsolutePath.make(
            path.isAbsolute(params.directory)
              ? params.directory
              : path.resolve(instance.directory, params.directory),
          )
          yield* moveSession
            .moveSession({ sessionID: ctx.sessionID, destination: { directory }, moveChanges: params.moveChanges ?? false })
            .pipe(Effect.mapError((error) => new Error(describeError(error))))
          return {
            title: `Changed directory to ${directory}`,
            output: `Successfully changed working directory to ${directory}`,
            metadata: { directory },
          }
        }).pipe(Effect.orDie),
    }
  }),
)

function describeError(error: MoveSession.Error): string {
  if (error instanceof SessionV2.NotFoundError) return `Session not found: ${error.sessionID}`
  if (error instanceof MoveSession.DestinationProjectMismatchError) return "Destination directory belongs to another project"
  if (error instanceof MoveSession.CaptureChangesError) return `Unable to capture changes in the source directory: ${error.message}`
  if (error instanceof MoveSession.ApplyChangesError) return `Unable to apply changes in the destination directory: ${error.message}`
  return `Unable to reset source changes in ${error.directory}: ${error.message}`
}
Reference implementation — Tool description text
Change the session's working directory. Use this when you need to work in a different directory, such as a different worktree or project subdirectory.

The directory can be an absolute path or relative to the current working directory. It must belong to the same project. Set moveChanges to true to transfer uncommitted changes to the destination directory.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.