Skip to content

Session tab / directory semantics: scoped locking and cwd pinning (with reference implementation) #513

Closed

Description

@wzj-zz

Context

I run a git-worktree workflow on top of opencode.nvim session tabs: each tab is bound to a different worktree directory of the same repo, so several agents can work in parallel with file isolation (related: #511, which asks for the underlying APIs).

The workflow works, but getting the tab/directory semantics right required wrapping two internals, which suggests the current tab design has gaps worth addressing natively.

Pain points

  1. session_locked is unconditional. When locked, :cd never switches the session — even to a completely different project. The natural semantics for directory-bound tabs is scoped: stay pinned while cwd moves within the same repo (main checkout and its linked worktrees), follow when cwd leaves for another repo.

  2. Locking keeps the session but not the directory. On directory change / panel reopen (handle_directory_change / check_cwd), the tab's current_cwd is reset to nvim's cwd even when the session is locked. Consequence: open_input_new_session (<C-n>) and @ completion resolve against nvim's cwd, not the session's directory — a new session created from a "locked" tab silently lands in the wrong directory.

  3. No entry point to open a session tab in a given directory. Covered by API requests: create session in a given directory, repo-scoped session lock, custom slash command registry #511, mentioning for completeness since it's part of the same picture.

Reference implementation

My config-level workaround (working well in daily use):

  • Wrap session_runtime.is_session_locked: for tabs created as worktree tabs, report locked only while vim.fn.getcwd() is under the tab's repo root.
  • Wrap state.context.set_current_cwd: ignore in-repo cwd changes for such tabs (keeps the tab pointing at its directory); when cwd leaves the repo, unpin the tab entirely so normal directory-follow takes over.

If this direction sounds reasonable I'm happy to help test or turn it into a PR.

Activity

  1. sudo-tee commented on Oct 9, 2026

    @sudo-tee
    Owner

    @wzj-zz

    I created a PR for the requested api.

    I tried to make them as agfnostic as possible to not enforce a perticular workflow,

    Let me know if they work for you.

    #517

  2. disrupted commented on Oct 9, 2026

    @disrupted
    Contributor

    @wzj-zz I am interested to learn more about your workflow as I am also dabbling with the idea of using worktrees inside Neovim. when you're referring to "opencode.nvim session tabs" what do you mean exactly? I am not aware of any "tab" concept within opencode.nvim, only with Neovim's native tabpages (:h tabpage)

  3. wzj-zz commented on Oct 10, 2026

    @wzj-zz
    ContributorAuthor

    Tested #517 on Windows 11 / Neovim 0.11 — the new APIs cover our workflow. Migrated my worktree integration from the two internal wrappers to:

    • require('opencode.api').open_session({ directory = ... })
    • lock_session_to_directory as a function (repo-wide same-repo / cross-repo rule)

    All bound-tab semantics work: pin inside the repo, unpin outside, completion and new sessions follow the bound directory.

    Two Windows issues in the PR itself:

    1. Session reuse misses on Windows. open_session without new compares the plugin-side directory string against server-side session paths. On Windows the port mapping rewrites those paths, so the lookup never hits and every open spawns a fresh session. I work around it by matching already-bound tabs first; the underlying server-side path mapping needs a fix.
    2. Two lock-policy unit tests fail on Windows because they hardcode /tmp/... paths; Windows normalizes them to C:/tmp/... and the string assertions miss.

    Also, with the public API in place, the worktree lock/unlock logic in my config is now entirely via lock_session_to_directory and open_session — no more monkey patches. LGTM pending the Windows path fix.

  4. wzj-zz commented on Oct 10, 2026

    @wzj-zz
    ContributorAuthor

    @disrupted the session tabs I mentioned are the plugin's logical panel tabs (created with , cycled with <M-,>/<M-.>), not Neovim tabpages. Workflow: lists all git worktrees plus bare branches; each worktree gets or reuses a tab bound to that directory; multiple agents work in parallel without file conflicts. When a task is done the agent merges the branch back and I delete the worktree with . Previously this required wrapping two internals (is_session_locked / set_current_cwd); with #517 it's all public API.

  5. wzj-zz commented on Oct 10, 2026

    @wzj-zz
    ContributorAuthor

    @disrupted one clarification on my earlier reply: <M-c> creates a new session in a new logical tab, not an empty tab — the plugin has no standalone "tab" object; each tab is a container for one session. So the workflow is: pick a worktree → open (or reopen) that directory's session in its own tab → work → delete when done.

  6. disrupted commented on Oct 10, 2026

    @disrupted
    Contributor

    @wzj-zz thanks for the explanation. I had no idea this feature existed haha

  7. sudo-tee commented on Oct 10, 2026

    @sudo-tee
    Owner

    Tested #517 on Windows 11 / Neovim 0.11 — the new APIs cover our workflow. Migrated my worktree integration from the two internal wrappers to:

    • require('opencode.api').open_session({ directory = ... })
    • lock_session_to_directory as a function (repo-wide same-repo / cross-repo rule)

    All bound-tab semantics work: pin inside the repo, unpin outside, completion and new sessions follow the bound directory.

    Two Windows issues in the PR itself:

    1. Session reuse misses on Windows. open_session without new compares the plugin-side directory string against server-side session paths. On Windows the port mapping rewrites those paths, so the lookup never hits and every open spawns a fresh session. I work around it by matching already-bound tabs first; the underlying server-side path mapping needs a fix.
    2. Two lock-policy unit tests fail on Windows because they hardcode /tmp/... paths; Windows normalizes them to C:/tmp/... and the string assertions miss.

    Also, with the public API in place, the worktree lock/unlock logic in my config is now entirely via lock_session_to_directory and open_session — no more monkey patches. LGTM pending the Windows path fix.

    @wzj-zz

    I pushed a fis for the 2 windows path issues you had.

    let me know if they work for you

  8. wzj-zz commented on Oct 10, 2026

    @wzj-zz
    ContributorAuthor

    @sudo-tee Fix verified on Windows 11 / Neovim 0.11.

    • tests/unit/directory_session_spec.lua — 9/9 pass (including the new mapped-directory cases)
    • tests/unit/lock_policy_spec.lua — 9/9 pass (the /tmp path issue is gone)

    The matches_directory helper comparing both the raw directory and the mapped server directory is the right shape. open_session reuse now hits correctly without my local tab-boundary workaround.

    LGTM to merge.

  9. sudo-tee commented on Oct 10, 2026

    @sudo-tee
    Owner

    That was fast :)

  10. wzj-zz commented on Oct 10, 2026

    @wzj-zz
    ContributorAuthor

    @sudo-tee Thanks for the quick turnaround — the worktree workflow is working beautifully now. Really appreciate all the effort on this.

  11. jensenojs commented on Oct 12, 2026

    @jensenojs
    Collaborator

    Would you be willing to write a recipe to contribute to our documentation? @wzj-zz

  12. sudo-tee commented on Oct 12, 2026

    @sudo-tee
    Owner

    @jensenojs

    I already have one in the MR for documentation rework IAM doing.

    #518

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions