From a3adaf475ec6f9763b1440f026811279f53e25dd Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Wed, 28 Jan 2026 23:48:25 -0500 Subject: [PATCH 1/9] Add Operations interface to internal/git - Define Operations interface with 7 methods (PushSetUpstream, Pull, Clone, FetchBranch, BranchExists, ValidateExistingBranch, ListBranches) - Create cliOperations struct implementing the interface - Add Default variable for production use - Keep existing functions as wrappers for backward compatibility Co-Authored-By: Claude Opus 4.5 --- internal/git/git.go | 100 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/internal/git/git.go b/internal/git/git.go index 9f4cef35..74f59086 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -7,8 +7,36 @@ import ( "strings" ) -// PushSetUpstreamInDir pushes the specified branch and sets upstream tracking. -func PushSetUpstreamInDir(ctx context.Context, branch, dir string) error { +// Operations defines the interface for git operations. +// This abstraction enables testing without actual git commands. +type Operations interface { + // PushSetUpstream pushes the specified branch and sets upstream tracking. + PushSetUpstream(ctx context.Context, branch, dir string) error + // Pull pulls the latest changes in a specific directory. + Pull(ctx context.Context, dir string) error + // Clone clones a repository from source to dest. + Clone(ctx context.Context, source, dest string) error + // FetchBranch fetches a specific branch from origin. + FetchBranch(ctx context.Context, repoPath, branch string) error + // BranchExists checks if a branch exists locally or remotely. + BranchExists(ctx context.Context, repoPath, branchName string) bool + // ValidateExistingBranch checks if a branch exists locally, remotely, or both. + ValidateExistingBranch(ctx context.Context, repoPath, branchName string) (existsLocal, existsRemote bool, err error) + // ListBranches returns a deduplicated list of all branches (local and remote). + ListBranches(ctx context.Context, repoPath string) ([]string, error) +} + +// cliOperations implements Operations using the git CLI. +type cliOperations struct{} + +// Compile-time check that cliOperations implements Operations. +var _ Operations = (*cliOperations)(nil) + +// Default is the default Operations implementation using the git CLI. +var Default Operations = &cliOperations{} + +// PushSetUpstream implements Operations.PushSetUpstream. +func (c *cliOperations) PushSetUpstream(ctx context.Context, branch, dir string) error { cmd := exec.CommandContext(ctx, "git", "push", "--set-upstream", "origin", branch) if dir != "" { cmd.Dir = dir @@ -19,8 +47,14 @@ func PushSetUpstreamInDir(ctx context.Context, branch, dir string) error { return nil } -// PullInDir pulls the latest changes in a specific directory. -func PullInDir(ctx context.Context, dir string) error { +// PushSetUpstreamInDir pushes the specified branch and sets upstream tracking. +// Deprecated: Use Default.PushSetUpstream instead. +func PushSetUpstreamInDir(ctx context.Context, branch, dir string) error { + return Default.PushSetUpstream(ctx, branch, dir) +} + +// Pull implements Operations.Pull. +func (c *cliOperations) Pull(ctx context.Context, dir string) error { cmd := exec.CommandContext(ctx, "git", "pull") if dir != "" { cmd.Dir = dir @@ -31,8 +65,14 @@ func PullInDir(ctx context.Context, dir string) error { return nil } -// Clone clones a repository from source to dest. -func Clone(ctx context.Context, source, dest string) error { +// PullInDir pulls the latest changes in a specific directory. +// Deprecated: Use Default.Pull instead. +func PullInDir(ctx context.Context, dir string) error { + return Default.Pull(ctx, dir) +} + +// Clone implements Operations.Clone. +func (c *cliOperations) Clone(ctx context.Context, source, dest string) error { cmd := exec.CommandContext(ctx, "git", "clone", source, dest) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to clone repository: %w\n%s", err, output) @@ -40,8 +80,14 @@ func Clone(ctx context.Context, source, dest string) error { return nil } -// FetchBranch fetches a specific branch from origin. -func FetchBranch(ctx context.Context, repoPath, branch string) error { +// Clone clones a repository from source to dest. +// Deprecated: Use Default.Clone instead. +func Clone(ctx context.Context, source, dest string) error { + return Default.Clone(ctx, source, dest) +} + +// FetchBranch implements Operations.FetchBranch. +func (c *cliOperations) FetchBranch(ctx context.Context, repoPath, branch string) error { cmd := exec.CommandContext(ctx, "git", "fetch", "origin", branch) cmd.Dir = repoPath if output, err := cmd.CombinedOutput(); err != nil { @@ -50,8 +96,14 @@ func FetchBranch(ctx context.Context, repoPath, branch string) error { return nil } -// BranchExists checks if a branch exists locally or remotely. -func BranchExists(ctx context.Context, repoPath, branchName string) bool { +// FetchBranch fetches a specific branch from origin. +// Deprecated: Use Default.FetchBranch instead. +func FetchBranch(ctx context.Context, repoPath, branch string) error { + return Default.FetchBranch(ctx, repoPath, branch) +} + +// BranchExists implements Operations.BranchExists. +func (c *cliOperations) BranchExists(ctx context.Context, repoPath, branchName string) bool { // Check local branches cmd := exec.CommandContext(ctx, "git", "show-ref", "--verify", "--quiet", "refs/heads/"+branchName) cmd.Dir = repoPath @@ -65,9 +117,14 @@ func BranchExists(ctx context.Context, repoPath, branchName string) bool { return cmd.Run() == nil } -// ValidateExistingBranch checks if a branch exists locally, remotely, or both. -// Returns (existsLocal, existsRemote, error). -func ValidateExistingBranch(ctx context.Context, repoPath, branchName string) (bool, bool, error) { +// BranchExists checks if a branch exists locally or remotely. +// Deprecated: Use Default.BranchExists instead. +func BranchExists(ctx context.Context, repoPath, branchName string) bool { + return Default.BranchExists(ctx, repoPath, branchName) +} + +// ValidateExistingBranch implements Operations.ValidateExistingBranch. +func (c *cliOperations) ValidateExistingBranch(ctx context.Context, repoPath, branchName string) (bool, bool, error) { // Check local branches cmd := exec.CommandContext(ctx, "git", "show-ref", "--verify", "--quiet", "refs/heads/"+branchName) cmd.Dir = repoPath @@ -81,9 +138,14 @@ func ValidateExistingBranch(ctx context.Context, repoPath, branchName string) (b return existsLocal, existsRemote, nil } -// ListBranches returns a deduplicated list of all branches (local and remote). -// Excludes HEAD and the current branch. Remote branches have their origin/ prefix stripped. -func ListBranches(ctx context.Context, repoPath string) ([]string, error) { +// ValidateExistingBranch checks if a branch exists locally, remotely, or both. +// Deprecated: Use Default.ValidateExistingBranch instead. +func ValidateExistingBranch(ctx context.Context, repoPath, branchName string) (bool, bool, error) { + return Default.ValidateExistingBranch(ctx, repoPath, branchName) +} + +// ListBranches implements Operations.ListBranches. +func (c *cliOperations) ListBranches(ctx context.Context, repoPath string) ([]string, error) { // Get local branches cmd := exec.CommandContext(ctx, "git", "branch", "--format=%(refname:short)") cmd.Dir = repoPath @@ -145,3 +207,9 @@ func ListBranches(ctx context.Context, repoPath string) ([]string, error) { return branches, nil } + +// ListBranches returns a deduplicated list of all branches (local and remote). +// Deprecated: Use Default.ListBranches instead. +func ListBranches(ctx context.Context, repoPath string) ([]string, error) { + return Default.ListBranches(ctx, repoPath) +} From 6526eeb1e6122e81680e560570020d674e90bcee Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Wed, 28 Jan 2026 23:49:38 -0500 Subject: [PATCH 2/9] Add Operations interface to internal/mise - Define Operations interface with 8 methods (IsManaged, Trust, Install, HasTask, RunTask, Exec, Initialize, InitializeWithOutput) - Create cliOperations struct implementing the interface - Add Default variable for production use - Keep existing functions as wrappers for backward compatibility Co-Authored-By: Claude Opus 4.5 --- internal/mise/mise.go | 121 +++++++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 24 deletions(-) diff --git a/internal/mise/mise.go b/internal/mise/mise.go index 726a25d7..ffe150cf 100644 --- a/internal/mise/mise.go +++ b/internal/mise/mise.go @@ -9,6 +9,37 @@ import ( "strings" ) +// Operations defines the interface for mise operations. +// This abstraction enables testing without actual mise commands. +type Operations interface { + // IsManaged returns true if the directory has a mise config file. + IsManaged(dir string) bool + // Trust runs `mise trust` in the given directory. + Trust(dir string) error + // Install runs `mise install` in the given directory. + Install(dir string) error + // HasTask checks if a mise task exists in the given directory. + HasTask(dir, taskName string) bool + // RunTask runs a mise task in the given directory. + RunTask(dir, taskName string) error + // Exec runs a command with the mise environment in the given directory. + Exec(dir, command string, args ...string) ([]byte, error) + // Initialize runs mise trust, install, and setup task if available. + Initialize(dir string) error + // InitializeWithOutput runs mise trust, install, and setup task if available, + // writing progress messages to the provided writer. + InitializeWithOutput(dir string, w io.Writer) error +} + +// cliOperations implements Operations using the mise CLI. +type cliOperations struct{} + +// Compile-time check that cliOperations implements Operations. +var _ Operations = (*cliOperations)(nil) + +// Default is the default Operations implementation using the mise CLI. +var Default Operations = &cliOperations{} + // configFiles lists all mise config file locations to check. var configFiles = []string{ ".mise.toml", @@ -28,13 +59,19 @@ func findConfigFile(dir string) string { return "" } +// IsManaged implements Operations.IsManaged. +func (c *cliOperations) IsManaged(dir string) bool { + return findConfigFile(dir) != "" +} + // IsManaged returns true if the directory has a mise config file. +// Deprecated: Use Default.IsManaged instead. func IsManaged(dir string) bool { - return findConfigFile(dir) != "" + return Default.IsManaged(dir) } -// Trust runs `mise trust` in the given directory. -func Trust(dir string) error { +// Trust implements Operations.Trust. +func (c *cliOperations) Trust(dir string) error { cmd := exec.Command("mise", "trust") cmd.Dir = dir if output, err := cmd.CombinedOutput(); err != nil { @@ -43,8 +80,14 @@ func Trust(dir string) error { return nil } -// Install runs `mise install` in the given directory. -func Install(dir string) error { +// Trust runs `mise trust` in the given directory. +// Deprecated: Use Default.Trust instead. +func Trust(dir string) error { + return Default.Trust(dir) +} + +// Install implements Operations.Install. +func (c *cliOperations) Install(dir string) error { cmd := exec.Command("mise", "install") cmd.Dir = dir if output, err := cmd.CombinedOutput(); err != nil { @@ -53,8 +96,14 @@ func Install(dir string) error { return nil } -// HasTask checks if a mise task exists in the given directory. -func HasTask(dir, taskName string) bool { +// Install runs `mise install` in the given directory. +// Deprecated: Use Default.Install instead. +func Install(dir string) error { + return Default.Install(dir) +} + +// HasTask implements Operations.HasTask. +func (c *cliOperations) HasTask(dir, taskName string) bool { cmd := exec.Command("mise", "task", "ls") cmd.Dir = dir output, err := cmd.Output() @@ -72,8 +121,14 @@ func HasTask(dir, taskName string) bool { return false } -// RunTask runs a mise task in the given directory. -func RunTask(dir, taskName string) error { +// HasTask checks if a mise task exists in the given directory. +// Deprecated: Use Default.HasTask instead. +func HasTask(dir, taskName string) bool { + return Default.HasTask(dir, taskName) +} + +// RunTask implements Operations.RunTask. +func (c *cliOperations) RunTask(dir, taskName string) error { cmd := exec.Command("mise", "run", taskName) cmd.Dir = dir if output, err := cmd.CombinedOutput(); err != nil { @@ -82,9 +137,14 @@ func RunTask(dir, taskName string) error { return nil } -// Exec runs a command with the mise environment in the given directory. -// This uses `mise exec -- command args...` to ensure mise-managed tools are available. -func Exec(dir, command string, args ...string) ([]byte, error) { +// RunTask runs a mise task in the given directory. +// Deprecated: Use Default.RunTask instead. +func RunTask(dir, taskName string) error { + return Default.RunTask(dir, taskName) +} + +// Exec implements Operations.Exec. +func (c *cliOperations) Exec(dir, command string, args ...string) ([]byte, error) { miseArgs := append([]string{"exec", "--"}, command) miseArgs = append(miseArgs, args...) // #nosec G204 -- command and args are from trusted internal callers @@ -97,18 +157,25 @@ func Exec(dir, command string, args ...string) ([]byte, error) { return output, nil } +// Exec runs a command with the mise environment in the given directory. +// Deprecated: Use Default.Exec instead. +func Exec(dir, command string, args ...string) ([]byte, error) { + return Default.Exec(dir, command, args...) +} + +// Initialize implements Operations.Initialize. +func (c *cliOperations) Initialize(dir string) error { + return c.InitializeWithOutput(dir, os.Stdout) +} + // Initialize runs mise trust, install, and setup task if available. -// Returns nil if mise is not enabled in the directory. -// Errors are returned but callers may choose to treat them as warnings. +// Deprecated: Use Default.Initialize instead. func Initialize(dir string) error { - return InitializeWithOutput(dir, os.Stdout) + return Default.Initialize(dir) } -// InitializeWithOutput runs mise trust, install, and setup task if available. -// Progress messages are written to the provided writer. -// Pass io.Discard to suppress output (useful for TUI contexts). -// Returns nil if mise is not enabled in the directory. -func InitializeWithOutput(dir string, w io.Writer) error { +// InitializeWithOutput implements Operations.InitializeWithOutput. +func (c *cliOperations) InitializeWithOutput(dir string, w io.Writer) error { configFile := findConfigFile(dir) if configFile == "" { fmt.Fprintf(w, " Mise: not enabled (no config file found)\n") @@ -118,19 +185,19 @@ func InitializeWithOutput(dir string, w io.Writer) error { fmt.Fprintf(w, " Mise: found %s\n", configFile) fmt.Fprintf(w, " Mise: running trust...\n") - if err := Trust(dir); err != nil { + if err := c.Trust(dir); err != nil { return err } fmt.Fprintf(w, " Mise: running install...\n") - if err := Install(dir); err != nil { + if err := c.Install(dir); err != nil { return err } // Run setup task if it exists - if HasTask(dir, "setup") { + if c.HasTask(dir, "setup") { fmt.Fprintf(w, " Mise: running setup task...\n") - if err := RunTask(dir, "setup"); err != nil { + if err := c.RunTask(dir, "setup"); err != nil { return err } } @@ -138,3 +205,9 @@ func InitializeWithOutput(dir string, w io.Writer) error { fmt.Fprintf(w, " Mise: initialization complete\n") return nil } + +// InitializeWithOutput runs mise trust, install, and setup task if available. +// Deprecated: Use Default.InitializeWithOutput instead. +func InitializeWithOutput(dir string, w io.Writer) error { + return Default.InitializeWithOutput(dir, w) +} From 52f0879a637333dbd3bc578c646b8a73243efe31 Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Wed, 28 Jan 2026 23:50:30 -0500 Subject: [PATCH 3/9] Add ClientInterface to internal/github - Define ClientInterface with 6 methods (GetPRStatus, PostPRComment, PostReplyToComment, PostReviewReply, ResolveReviewThread, GetJobLogs) - Add compile-time check that Client implements ClientInterface - Update internal/feedback to accept interface instead of *Client Co-Authored-By: Claude Opus 4.5 --- internal/feedback/integration.go | 2 +- internal/feedback/processor.go | 6 +++--- internal/github/client.go | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/feedback/integration.go b/internal/feedback/integration.go index 66b26d48..00f1471e 100644 --- a/internal/feedback/integration.go +++ b/internal/feedback/integration.go @@ -26,7 +26,7 @@ type BeadInfo struct { // Integration handles the integration between GitHub PR feedback and beads. type Integration struct { - client *github.Client + client github.ClientInterface processor *FeedbackProcessor } diff --git a/internal/feedback/processor.go b/internal/feedback/processor.go index 66855f7d..f51572c4 100644 --- a/internal/feedback/processor.go +++ b/internal/feedback/processor.go @@ -18,14 +18,14 @@ const maxLogContentSize = 50 * 1024 // 50KB // FeedbackProcessor processes PR feedback and generates actionable items. type FeedbackProcessor struct { - client *github.Client + client github.ClientInterface // Optional fields for Claude log analysis integration proj *project.Project workID string } // NewFeedbackProcessor creates a new feedback processor. -func NewFeedbackProcessor(client *github.Client) *FeedbackProcessor { +func NewFeedbackProcessor(client github.ClientInterface) *FeedbackProcessor { return &FeedbackProcessor{ client: client, } @@ -33,7 +33,7 @@ func NewFeedbackProcessor(client *github.Client) *FeedbackProcessor { // NewFeedbackProcessorWithProject creates a feedback processor with project context. // This enables Claude-based log analysis when configured. -func NewFeedbackProcessorWithProject(client *github.Client, proj *project.Project, workID string) *FeedbackProcessor { +func NewFeedbackProcessorWithProject(client github.ClientInterface, proj *project.Project, workID string) *FeedbackProcessor { return &FeedbackProcessor{ client: client, proj: proj, diff --git a/internal/github/client.go b/internal/github/client.go index 7104d6c4..c8f8ca4c 100644 --- a/internal/github/client.go +++ b/internal/github/client.go @@ -13,9 +13,29 @@ import ( "github.com/newhook/co/internal/logging" ) +// ClientInterface defines the interface for GitHub API operations. +// This abstraction enables testing without actual GitHub API calls. +type ClientInterface interface { + // GetPRStatus fetches comprehensive PR status information. + GetPRStatus(ctx context.Context, prURL string) (*PRStatus, error) + // PostPRComment posts a comment on a PR issue. + PostPRComment(ctx context.Context, prURL string, body string) error + // PostReplyToComment posts a reply to a specific comment on a PR. + PostReplyToComment(ctx context.Context, prURL string, commentID int, body string) error + // PostReviewReply posts a reply to a review comment. + PostReviewReply(ctx context.Context, prURL string, reviewCommentID int, body string) error + // ResolveReviewThread resolves a review thread containing the specified comment. + ResolveReviewThread(ctx context.Context, prURL string, commentID int) error + // GetJobLogs fetches the logs for a specific job. + GetJobLogs(ctx context.Context, repo string, jobID int64) (string, error) +} + // Client wraps the gh CLI for GitHub API operations. type Client struct{} +// Compile-time check that Client implements ClientInterface. +var _ ClientInterface = (*Client)(nil) + // NewClient creates a new GitHub client. func NewClient() *Client { return &Client{} From e3a10dbecf11387f9574d3c0d0d552c46d116f3f Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Wed, 28 Jan 2026 23:51:22 -0500 Subject: [PATCH 4/9] Add ClientInterface to internal/zellij - Define ClientInterface covering session, tab, and pane operations - Add compile-time check that Client implements ClientInterface - Update internal/tui to accept interface instead of *Client Co-Authored-By: Claude Opus 4.5 --- internal/tui/tui_plan.go | 2 +- internal/zellij/zellij.go | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/internal/tui/tui_plan.go b/internal/tui/tui_plan.go index 538f2d15..b7f55267 100644 --- a/internal/tui/tui_plan.go +++ b/internal/tui/tui_plan.go @@ -102,7 +102,7 @@ type planModel struct { // Per-bead session tracking activeBeadSessions map[string]bool // beadID -> has active session - zj *zellij.Client + zj zellij.ClientInterface // Two-column layout settings columnRatio float64 // Ratio of issues column width (0.0-1.0), default 0.4 for 40/60 split diff --git a/internal/zellij/zellij.go b/internal/zellij/zellij.go index 92ef3544..89be5e2c 100644 --- a/internal/zellij/zellij.go +++ b/internal/zellij/zellij.go @@ -51,6 +51,45 @@ func IsInsideTargetSession(session string) bool { return CurrentSessionName() == session } +// ClientInterface defines the interface for zellij operations. +// This abstraction enables testing without actual zellij commands. +type ClientInterface interface { + // Session management + ListSessions(ctx context.Context) ([]string, error) + SessionExists(ctx context.Context, name string) (bool, error) + IsSessionActive(ctx context.Context, name string) (bool, error) + CreateSession(ctx context.Context, name string) error + CreateSessionWithLayout(ctx context.Context, name string, projectRoot string) error + DeleteSession(ctx context.Context, name string) error + EnsureSession(ctx context.Context, name string) (bool, error) + EnsureSessionWithLayout(ctx context.Context, name string, projectRoot string) (bool, error) + + // Tab management + CreateTab(ctx context.Context, session, name, cwd string) error + CreateTabWithCommand(ctx context.Context, session, name, cwd, command string, args []string, paneName string) error + SwitchToTab(ctx context.Context, session, name string) error + QueryTabNames(ctx context.Context, session string) ([]string, error) + TabExists(ctx context.Context, session, name string) (bool, error) + CloseTab(ctx context.Context, session string) error + + // Pane input control + WriteASCII(ctx context.Context, session string, code int) error + WriteChars(ctx context.Context, session, text string) error + SendCtrlC(ctx context.Context, session string) error + SendEnter(ctx context.Context, session string) error + ExecuteCommand(ctx context.Context, session, cmd string) error + + // High-level operations + TerminateProcess(ctx context.Context, session string) error + ClearAndExecute(ctx context.Context, session, cmd string) error + TerminateAndCloseTab(ctx context.Context, session, tabName string) error + + // Floating pane operations + RunFloating(ctx context.Context, session, name, cwd string, command ...string) error + ToggleFloatingPanes(ctx context.Context, session string) error + Run(ctx context.Context, session, name, cwd string, command ...string) error +} + // Client provides methods for interacting with zellij sessions, tabs, and panes. type Client struct { // Timeouts for various operations @@ -60,6 +99,9 @@ type Client struct { SessionStartWait time.Duration } +// Compile-time check that Client implements ClientInterface. +var _ ClientInterface = (*Client)(nil) + // New creates a new zellij client with default configuration. func New() *Client { return &Client{ From 324313b5fbfccab2cb4deb05c5538767aaa18523 Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Wed, 28 Jan 2026 23:53:02 -0500 Subject: [PATCH 5/9] Add CLI and Reader interfaces to internal/beads Created cli.go with: - CLI interface for bd command operations (Init, InstallHooks, Create, Close, Reopen, Update, AddComment, AddLabels, SetExternalRef, AddDependency) - Reader interface for database queries (GetBead, GetBeadsWithDeps, ListBeads, GetReadyBeads, GetTransitiveDependencies, GetBeadWithChildren) - cliImpl struct implementing CLI using the bd command - DefaultCLI variable for the default implementation - Compile-time check that Client implements Reader Co-Authored-By: Claude Opus 4.5 --- internal/beads/cli.go | 109 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 internal/beads/cli.go diff --git a/internal/beads/cli.go b/internal/beads/cli.go new file mode 100644 index 00000000..fc3e35a6 --- /dev/null +++ b/internal/beads/cli.go @@ -0,0 +1,109 @@ +package beads + +import ( + "context" +) + +// CLI defines the interface for bd command operations. +// This abstraction enables testing without actual bd CLI calls. +type CLI interface { + // Init initializes beads in the specified directory. + Init(ctx context.Context, beadsDir, prefix string) error + // InstallHooks installs beads hooks in the specified directory. + InstallHooks(ctx context.Context, repoDir string) error + // Create creates a new bead and returns its ID. + Create(ctx context.Context, beadsDir string, opts CreateOptions) (string, error) + // Close closes a bead. + Close(ctx context.Context, beadID, beadsDir string) error + // Reopen reopens a closed bead. + Reopen(ctx context.Context, beadID, beadsDir string) error + // Update updates a bead's fields. + Update(ctx context.Context, beadID, beadsDir string, opts UpdateOptions) error + // AddComment adds a comment to a bead. + AddComment(ctx context.Context, beadID, comment, beadsDir string) error + // AddLabels adds labels to a bead. + AddLabels(ctx context.Context, beadID, beadsDir string, labels []string) error + // SetExternalRef sets the external reference for a bead. + SetExternalRef(ctx context.Context, beadID, externalRef, beadsDir string) error + // AddDependency adds a dependency between two beads. + AddDependency(ctx context.Context, beadID, dependsOnID, beadsDir string) error +} + +// Reader defines the interface for reading beads from the database. +// This abstraction enables testing without actual database access. +type Reader interface { + // GetBead retrieves a single bead by ID with its dependencies/dependents. + GetBead(ctx context.Context, id string) (*BeadWithDeps, error) + // GetBeadsWithDeps retrieves beads and their dependencies/dependents. + GetBeadsWithDeps(ctx context.Context, beadIDs []string) (*BeadsWithDepsResult, error) + // ListBeads lists all beads with optional status filter. + ListBeads(ctx context.Context, status string) ([]Bead, error) + // GetReadyBeads returns all open beads where all dependencies are satisfied. + GetReadyBeads(ctx context.Context) ([]Bead, error) + // GetTransitiveDependencies collects all transitive dependencies for a bead. + GetTransitiveDependencies(ctx context.Context, id string) ([]Bead, error) + // GetBeadWithChildren retrieves a bead and all its child beads recursively. + GetBeadWithChildren(ctx context.Context, id string) ([]Bead, error) +} + +// cliImpl implements CLI using the bd command-line tool. +type cliImpl struct{} + +// Compile-time check that cliImpl implements CLI. +var _ CLI = (*cliImpl)(nil) + +// DefaultCLI is the default CLI implementation using the bd command. +var DefaultCLI CLI = &cliImpl{} + +// Init implements CLI.Init. +func (c *cliImpl) Init(ctx context.Context, beadsDir, prefix string) error { + return Init(ctx, beadsDir, prefix) +} + +// InstallHooks implements CLI.InstallHooks. +func (c *cliImpl) InstallHooks(ctx context.Context, repoDir string) error { + return InstallHooks(ctx, repoDir) +} + +// Create implements CLI.Create. +func (c *cliImpl) Create(ctx context.Context, beadsDir string, opts CreateOptions) (string, error) { + return Create(ctx, beadsDir, opts) +} + +// Close implements CLI.Close. +func (c *cliImpl) Close(ctx context.Context, beadID, beadsDir string) error { + return Close(ctx, beadID, beadsDir) +} + +// Reopen implements CLI.Reopen. +func (c *cliImpl) Reopen(ctx context.Context, beadID, beadsDir string) error { + return Reopen(ctx, beadID, beadsDir) +} + +// Update implements CLI.Update. +func (c *cliImpl) Update(ctx context.Context, beadID, beadsDir string, opts UpdateOptions) error { + return Update(ctx, beadID, beadsDir, opts) +} + +// AddComment implements CLI.AddComment. +func (c *cliImpl) AddComment(ctx context.Context, beadID, comment, beadsDir string) error { + return AddComment(ctx, beadID, comment, beadsDir) +} + +// AddLabels implements CLI.AddLabels. +func (c *cliImpl) AddLabels(ctx context.Context, beadID, beadsDir string, labels []string) error { + return AddLabels(ctx, beadID, beadsDir, labels) +} + +// SetExternalRef implements CLI.SetExternalRef. +func (c *cliImpl) SetExternalRef(ctx context.Context, beadID, externalRef, beadsDir string) error { + return SetExternalRef(ctx, beadID, externalRef, beadsDir) +} + +// AddDependency implements CLI.AddDependency. +func (c *cliImpl) AddDependency(ctx context.Context, beadID, dependsOnID, beadsDir string) error { + return AddDependency(ctx, beadID, dependsOnID, beadsDir) +} + +// Compile-time check that Client implements Reader. +var _ Reader = (*Client)(nil) From 2a283cf0704ced03b470b7315fc9471a78884cc1 Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Thu, 29 Jan 2026 08:47:33 -0500 Subject: [PATCH 6/9] Refactor beads CLI interface to bind beadsDir at construction Move beadsDir from method parameters to struct field, matching the Reader/Client pattern. Init and InstallHooks remain package-level functions as they're setup operations. Co-Authored-By: Claude Opus 4.5 --- internal/beads/cli.go | 74 ++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/internal/beads/cli.go b/internal/beads/cli.go index fc3e35a6..481692ee 100644 --- a/internal/beads/cli.go +++ b/internal/beads/cli.go @@ -6,27 +6,27 @@ import ( // CLI defines the interface for bd command operations. // This abstraction enables testing without actual bd CLI calls. +// Each CLI instance is bound to a specific beads directory. +// +// Note: Init and InstallHooks are package-level functions, not part of this +// interface, since they are setup operations that run before a CLI is created. type CLI interface { - // Init initializes beads in the specified directory. - Init(ctx context.Context, beadsDir, prefix string) error - // InstallHooks installs beads hooks in the specified directory. - InstallHooks(ctx context.Context, repoDir string) error // Create creates a new bead and returns its ID. - Create(ctx context.Context, beadsDir string, opts CreateOptions) (string, error) + Create(ctx context.Context, opts CreateOptions) (string, error) // Close closes a bead. - Close(ctx context.Context, beadID, beadsDir string) error + Close(ctx context.Context, beadID string) error // Reopen reopens a closed bead. - Reopen(ctx context.Context, beadID, beadsDir string) error + Reopen(ctx context.Context, beadID string) error // Update updates a bead's fields. - Update(ctx context.Context, beadID, beadsDir string, opts UpdateOptions) error + Update(ctx context.Context, beadID string, opts UpdateOptions) error // AddComment adds a comment to a bead. - AddComment(ctx context.Context, beadID, comment, beadsDir string) error + AddComment(ctx context.Context, beadID, comment string) error // AddLabels adds labels to a bead. - AddLabels(ctx context.Context, beadID, beadsDir string, labels []string) error + AddLabels(ctx context.Context, beadID string, labels []string) error // SetExternalRef sets the external reference for a bead. - SetExternalRef(ctx context.Context, beadID, externalRef, beadsDir string) error + SetExternalRef(ctx context.Context, beadID, externalRef string) error // AddDependency adds a dependency between two beads. - AddDependency(ctx context.Context, beadID, dependsOnID, beadsDir string) error + AddDependency(ctx context.Context, beadID, dependsOnID string) error } // Reader defines the interface for reading beads from the database. @@ -47,62 +47,56 @@ type Reader interface { } // cliImpl implements CLI using the bd command-line tool. -type cliImpl struct{} +type cliImpl struct { + beadsDir string +} // Compile-time check that cliImpl implements CLI. var _ CLI = (*cliImpl)(nil) -// DefaultCLI is the default CLI implementation using the bd command. -var DefaultCLI CLI = &cliImpl{} - -// Init implements CLI.Init. -func (c *cliImpl) Init(ctx context.Context, beadsDir, prefix string) error { - return Init(ctx, beadsDir, prefix) -} - -// InstallHooks implements CLI.InstallHooks. -func (c *cliImpl) InstallHooks(ctx context.Context, repoDir string) error { - return InstallHooks(ctx, repoDir) +// NewCLI creates a new CLI instance bound to the specified beads directory. +func NewCLI(beadsDir string) CLI { + return &cliImpl{beadsDir: beadsDir} } // Create implements CLI.Create. -func (c *cliImpl) Create(ctx context.Context, beadsDir string, opts CreateOptions) (string, error) { - return Create(ctx, beadsDir, opts) +func (c *cliImpl) Create(ctx context.Context, opts CreateOptions) (string, error) { + return Create(ctx, c.beadsDir, opts) } // Close implements CLI.Close. -func (c *cliImpl) Close(ctx context.Context, beadID, beadsDir string) error { - return Close(ctx, beadID, beadsDir) +func (c *cliImpl) Close(ctx context.Context, beadID string) error { + return Close(ctx, beadID, c.beadsDir) } // Reopen implements CLI.Reopen. -func (c *cliImpl) Reopen(ctx context.Context, beadID, beadsDir string) error { - return Reopen(ctx, beadID, beadsDir) +func (c *cliImpl) Reopen(ctx context.Context, beadID string) error { + return Reopen(ctx, beadID, c.beadsDir) } // Update implements CLI.Update. -func (c *cliImpl) Update(ctx context.Context, beadID, beadsDir string, opts UpdateOptions) error { - return Update(ctx, beadID, beadsDir, opts) +func (c *cliImpl) Update(ctx context.Context, beadID string, opts UpdateOptions) error { + return Update(ctx, beadID, c.beadsDir, opts) } // AddComment implements CLI.AddComment. -func (c *cliImpl) AddComment(ctx context.Context, beadID, comment, beadsDir string) error { - return AddComment(ctx, beadID, comment, beadsDir) +func (c *cliImpl) AddComment(ctx context.Context, beadID, comment string) error { + return AddComment(ctx, beadID, comment, c.beadsDir) } // AddLabels implements CLI.AddLabels. -func (c *cliImpl) AddLabels(ctx context.Context, beadID, beadsDir string, labels []string) error { - return AddLabels(ctx, beadID, beadsDir, labels) +func (c *cliImpl) AddLabels(ctx context.Context, beadID string, labels []string) error { + return AddLabels(ctx, beadID, c.beadsDir, labels) } // SetExternalRef implements CLI.SetExternalRef. -func (c *cliImpl) SetExternalRef(ctx context.Context, beadID, externalRef, beadsDir string) error { - return SetExternalRef(ctx, beadID, externalRef, beadsDir) +func (c *cliImpl) SetExternalRef(ctx context.Context, beadID, externalRef string) error { + return SetExternalRef(ctx, beadID, externalRef, c.beadsDir) } // AddDependency implements CLI.AddDependency. -func (c *cliImpl) AddDependency(ctx context.Context, beadID, dependsOnID, beadsDir string) error { - return AddDependency(ctx, beadID, dependsOnID, beadsDir) +func (c *cliImpl) AddDependency(ctx context.Context, beadID, dependsOnID string) error { + return AddDependency(ctx, beadID, dependsOnID, c.beadsDir) } // Compile-time check that Client implements Reader. From 9aa030f1571570e1b0025f11c944420f058d6f5f Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Thu, 29 Jan 2026 09:06:33 -0500 Subject: [PATCH 7/9] Refactor mise Operations interface to bind dir at construction Move dir from method parameters to struct field, matching the beads CLI pattern. Package-level functions remain for convenience. Co-Authored-By: Claude Opus 4.5 --- internal/mise/mise.go | 101 ++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/internal/mise/mise.go b/internal/mise/mise.go index ffe150cf..6141a04c 100644 --- a/internal/mise/mise.go +++ b/internal/mise/mise.go @@ -11,34 +11,39 @@ import ( // Operations defines the interface for mise operations. // This abstraction enables testing without actual mise commands. +// Each Operations instance is bound to a specific directory. type Operations interface { // IsManaged returns true if the directory has a mise config file. - IsManaged(dir string) bool - // Trust runs `mise trust` in the given directory. - Trust(dir string) error - // Install runs `mise install` in the given directory. - Install(dir string) error - // HasTask checks if a mise task exists in the given directory. - HasTask(dir, taskName string) bool - // RunTask runs a mise task in the given directory. - RunTask(dir, taskName string) error - // Exec runs a command with the mise environment in the given directory. - Exec(dir, command string, args ...string) ([]byte, error) + IsManaged() bool + // Trust runs `mise trust` in the directory. + Trust() error + // Install runs `mise install` in the directory. + Install() error + // HasTask checks if a mise task exists in the directory. + HasTask(taskName string) bool + // RunTask runs a mise task in the directory. + RunTask(taskName string) error + // Exec runs a command with the mise environment in the directory. + Exec(command string, args ...string) ([]byte, error) // Initialize runs mise trust, install, and setup task if available. - Initialize(dir string) error + Initialize() error // InitializeWithOutput runs mise trust, install, and setup task if available, // writing progress messages to the provided writer. - InitializeWithOutput(dir string, w io.Writer) error + InitializeWithOutput(w io.Writer) error } // cliOperations implements Operations using the mise CLI. -type cliOperations struct{} +type cliOperations struct { + dir string +} // Compile-time check that cliOperations implements Operations. var _ Operations = (*cliOperations)(nil) -// Default is the default Operations implementation using the mise CLI. -var Default Operations = &cliOperations{} +// NewOperations creates a new Operations instance bound to the specified directory. +func NewOperations(dir string) Operations { + return &cliOperations{dir: dir} +} // configFiles lists all mise config file locations to check. var configFiles = []string{ @@ -60,20 +65,19 @@ func findConfigFile(dir string) string { } // IsManaged implements Operations.IsManaged. -func (c *cliOperations) IsManaged(dir string) bool { - return findConfigFile(dir) != "" +func (c *cliOperations) IsManaged() bool { + return findConfigFile(c.dir) != "" } // IsManaged returns true if the directory has a mise config file. -// Deprecated: Use Default.IsManaged instead. func IsManaged(dir string) bool { - return Default.IsManaged(dir) + return NewOperations(dir).IsManaged() } // Trust implements Operations.Trust. -func (c *cliOperations) Trust(dir string) error { +func (c *cliOperations) Trust() error { cmd := exec.Command("mise", "trust") - cmd.Dir = dir + cmd.Dir = c.dir if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("mise trust failed: %w\n%s", err, output) } @@ -81,15 +85,14 @@ func (c *cliOperations) Trust(dir string) error { } // Trust runs `mise trust` in the given directory. -// Deprecated: Use Default.Trust instead. func Trust(dir string) error { - return Default.Trust(dir) + return NewOperations(dir).Trust() } // Install implements Operations.Install. -func (c *cliOperations) Install(dir string) error { +func (c *cliOperations) Install() error { cmd := exec.Command("mise", "install") - cmd.Dir = dir + cmd.Dir = c.dir if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("mise install failed: %w\n%s", err, output) } @@ -97,15 +100,14 @@ func (c *cliOperations) Install(dir string) error { } // Install runs `mise install` in the given directory. -// Deprecated: Use Default.Install instead. func Install(dir string) error { - return Default.Install(dir) + return NewOperations(dir).Install() } // HasTask implements Operations.HasTask. -func (c *cliOperations) HasTask(dir, taskName string) bool { +func (c *cliOperations) HasTask(taskName string) bool { cmd := exec.Command("mise", "task", "ls") - cmd.Dir = dir + cmd.Dir = c.dir output, err := cmd.Output() if err != nil { return false @@ -122,15 +124,14 @@ func (c *cliOperations) HasTask(dir, taskName string) bool { } // HasTask checks if a mise task exists in the given directory. -// Deprecated: Use Default.HasTask instead. func HasTask(dir, taskName string) bool { - return Default.HasTask(dir, taskName) + return NewOperations(dir).HasTask(taskName) } // RunTask implements Operations.RunTask. -func (c *cliOperations) RunTask(dir, taskName string) error { +func (c *cliOperations) RunTask(taskName string) error { cmd := exec.Command("mise", "run", taskName) - cmd.Dir = dir + cmd.Dir = c.dir if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("mise run %s failed: %w\n%s", taskName, err, output) } @@ -138,18 +139,17 @@ func (c *cliOperations) RunTask(dir, taskName string) error { } // RunTask runs a mise task in the given directory. -// Deprecated: Use Default.RunTask instead. func RunTask(dir, taskName string) error { - return Default.RunTask(dir, taskName) + return NewOperations(dir).RunTask(taskName) } // Exec implements Operations.Exec. -func (c *cliOperations) Exec(dir, command string, args ...string) ([]byte, error) { +func (c *cliOperations) Exec(command string, args ...string) ([]byte, error) { miseArgs := append([]string{"exec", "--"}, command) miseArgs = append(miseArgs, args...) // #nosec G204 -- command and args are from trusted internal callers cmd := exec.Command("mise", miseArgs...) - cmd.Dir = dir + cmd.Dir = c.dir output, err := cmd.CombinedOutput() if err != nil { return output, fmt.Errorf("mise exec %s failed: %w\n%s", command, err, output) @@ -158,25 +158,23 @@ func (c *cliOperations) Exec(dir, command string, args ...string) ([]byte, error } // Exec runs a command with the mise environment in the given directory. -// Deprecated: Use Default.Exec instead. func Exec(dir, command string, args ...string) ([]byte, error) { - return Default.Exec(dir, command, args...) + return NewOperations(dir).Exec(command, args...) } // Initialize implements Operations.Initialize. -func (c *cliOperations) Initialize(dir string) error { - return c.InitializeWithOutput(dir, os.Stdout) +func (c *cliOperations) Initialize() error { + return c.InitializeWithOutput(os.Stdout) } // Initialize runs mise trust, install, and setup task if available. -// Deprecated: Use Default.Initialize instead. func Initialize(dir string) error { - return Default.Initialize(dir) + return NewOperations(dir).Initialize() } // InitializeWithOutput implements Operations.InitializeWithOutput. -func (c *cliOperations) InitializeWithOutput(dir string, w io.Writer) error { - configFile := findConfigFile(dir) +func (c *cliOperations) InitializeWithOutput(w io.Writer) error { + configFile := findConfigFile(c.dir) if configFile == "" { fmt.Fprintf(w, " Mise: not enabled (no config file found)\n") return nil @@ -185,19 +183,19 @@ func (c *cliOperations) InitializeWithOutput(dir string, w io.Writer) error { fmt.Fprintf(w, " Mise: found %s\n", configFile) fmt.Fprintf(w, " Mise: running trust...\n") - if err := c.Trust(dir); err != nil { + if err := c.Trust(); err != nil { return err } fmt.Fprintf(w, " Mise: running install...\n") - if err := c.Install(dir); err != nil { + if err := c.Install(); err != nil { return err } // Run setup task if it exists - if c.HasTask(dir, "setup") { + if c.HasTask("setup") { fmt.Fprintf(w, " Mise: running setup task...\n") - if err := c.RunTask(dir, "setup"); err != nil { + if err := c.RunTask("setup"); err != nil { return err } } @@ -207,7 +205,6 @@ func (c *cliOperations) InitializeWithOutput(dir string, w io.Writer) error { } // InitializeWithOutput runs mise trust, install, and setup task if available. -// Deprecated: Use Default.InitializeWithOutput instead. func InitializeWithOutput(dir string, w io.Writer) error { - return Default.InitializeWithOutput(dir, w) + return NewOperations(dir).InitializeWithOutput(w) } From cb4b6b65e38dd47710717c293af5b277707876c0 Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Thu, 29 Jan 2026 09:09:15 -0500 Subject: [PATCH 8/9] Split zellij ClientInterface into SessionManager and Session SessionManager handles session lifecycle (list, create, delete). Session is bound to a specific session name and handles tabs, panes, and input operations without repeating the session parameter. ClientInterface remains for backward compatibility, embedding both. Co-Authored-By: Claude Opus 4.5 --- internal/zellij/zellij.go | 138 +++++++++++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 8 deletions(-) diff --git a/internal/zellij/zellij.go b/internal/zellij/zellij.go index 89be5e2c..99e28228 100644 --- a/internal/zellij/zellij.go +++ b/internal/zellij/zellij.go @@ -51,10 +51,9 @@ func IsInsideTargetSession(session string) bool { return CurrentSessionName() == session } -// ClientInterface defines the interface for zellij operations. +// SessionManager defines the interface for managing zellij sessions. // This abstraction enables testing without actual zellij commands. -type ClientInterface interface { - // Session management +type SessionManager interface { ListSessions(ctx context.Context) ([]string, error) SessionExists(ctx context.Context, name string) (bool, error) IsSessionActive(ctx context.Context, name string) (bool, error) @@ -64,7 +63,45 @@ type ClientInterface interface { EnsureSession(ctx context.Context, name string) (bool, error) EnsureSessionWithLayout(ctx context.Context, name string, projectRoot string) (bool, error) + // Session returns a Session interface bound to the specified session name. + Session(name string) Session +} + +// Session defines the interface for operations within a specific zellij session. +// Each Session instance is bound to a specific session name. +type Session interface { // Tab management + CreateTab(ctx context.Context, name, cwd string) error + CreateTabWithCommand(ctx context.Context, name, cwd, command string, args []string, paneName string) error + SwitchToTab(ctx context.Context, name string) error + QueryTabNames(ctx context.Context) ([]string, error) + TabExists(ctx context.Context, name string) (bool, error) + CloseTab(ctx context.Context) error + + // Pane input control + WriteASCII(ctx context.Context, code int) error + WriteChars(ctx context.Context, text string) error + SendCtrlC(ctx context.Context) error + SendEnter(ctx context.Context) error + ExecuteCommand(ctx context.Context, cmd string) error + + // High-level operations + TerminateProcess(ctx context.Context) error + ClearAndExecute(ctx context.Context, cmd string) error + TerminateAndCloseTab(ctx context.Context, tabName string) error + + // Floating pane operations + RunFloating(ctx context.Context, name, cwd string, command ...string) error + ToggleFloatingPanes(ctx context.Context) error + Run(ctx context.Context, name, cwd string, command ...string) error +} + +// ClientInterface defines the combined interface for zellij operations. +// Deprecated: Use SessionManager and Session interfaces instead. +type ClientInterface interface { + SessionManager + + // Tab management (deprecated - use Session interface) CreateTab(ctx context.Context, session, name, cwd string) error CreateTabWithCommand(ctx context.Context, session, name, cwd, command string, args []string, paneName string) error SwitchToTab(ctx context.Context, session, name string) error @@ -72,19 +109,19 @@ type ClientInterface interface { TabExists(ctx context.Context, session, name string) (bool, error) CloseTab(ctx context.Context, session string) error - // Pane input control + // Pane input control (deprecated - use Session interface) WriteASCII(ctx context.Context, session string, code int) error WriteChars(ctx context.Context, session, text string) error SendCtrlC(ctx context.Context, session string) error SendEnter(ctx context.Context, session string) error ExecuteCommand(ctx context.Context, session, cmd string) error - // High-level operations + // High-level operations (deprecated - use Session interface) TerminateProcess(ctx context.Context, session string) error ClearAndExecute(ctx context.Context, session, cmd string) error TerminateAndCloseTab(ctx context.Context, session, tabName string) error - // Floating pane operations + // Floating pane operations (deprecated - use Session interface) RunFloating(ctx context.Context, session, name, cwd string, command ...string) error ToggleFloatingPanes(ctx context.Context, session string) error Run(ctx context.Context, session, name, cwd string, command ...string) error @@ -99,8 +136,12 @@ type Client struct { SessionStartWait time.Duration } -// Compile-time check that Client implements ClientInterface. -var _ ClientInterface = (*Client)(nil) +// Compile-time checks. +var ( + _ ClientInterface = (*Client)(nil) + _ SessionManager = (*Client)(nil) + _ Session = (*session)(nil) +) // New creates a new zellij client with default configuration. func New() *Client { @@ -112,6 +153,87 @@ func New() *Client { } } +// session implements the Session interface for a specific zellij session. +type session struct { + client *Client + name string +} + +// Session returns a Session interface bound to the specified session name. +func (c *Client) Session(name string) Session { + return &session{client: c, name: name} +} + +// Session interface implementations for session struct + +func (s *session) CreateTab(ctx context.Context, name, cwd string) error { + return s.client.CreateTab(ctx, s.name, name, cwd) +} + +func (s *session) CreateTabWithCommand(ctx context.Context, name, cwd, command string, args []string, paneName string) error { + return s.client.CreateTabWithCommand(ctx, s.name, name, cwd, command, args, paneName) +} + +func (s *session) SwitchToTab(ctx context.Context, name string) error { + return s.client.SwitchToTab(ctx, s.name, name) +} + +func (s *session) QueryTabNames(ctx context.Context) ([]string, error) { + return s.client.QueryTabNames(ctx, s.name) +} + +func (s *session) TabExists(ctx context.Context, name string) (bool, error) { + return s.client.TabExists(ctx, s.name, name) +} + +func (s *session) CloseTab(ctx context.Context) error { + return s.client.CloseTab(ctx, s.name) +} + +func (s *session) WriteASCII(ctx context.Context, code int) error { + return s.client.WriteASCII(ctx, s.name, code) +} + +func (s *session) WriteChars(ctx context.Context, text string) error { + return s.client.WriteChars(ctx, s.name, text) +} + +func (s *session) SendCtrlC(ctx context.Context) error { + return s.client.SendCtrlC(ctx, s.name) +} + +func (s *session) SendEnter(ctx context.Context) error { + return s.client.SendEnter(ctx, s.name) +} + +func (s *session) ExecuteCommand(ctx context.Context, cmd string) error { + return s.client.ExecuteCommand(ctx, s.name, cmd) +} + +func (s *session) TerminateProcess(ctx context.Context) error { + return s.client.TerminateProcess(ctx, s.name) +} + +func (s *session) ClearAndExecute(ctx context.Context, cmd string) error { + return s.client.ClearAndExecute(ctx, s.name, cmd) +} + +func (s *session) TerminateAndCloseTab(ctx context.Context, tabName string) error { + return s.client.TerminateAndCloseTab(ctx, s.name, tabName) +} + +func (s *session) RunFloating(ctx context.Context, name, cwd string, command ...string) error { + return s.client.RunFloating(ctx, s.name, name, cwd, command...) +} + +func (s *session) ToggleFloatingPanes(ctx context.Context) error { + return s.client.ToggleFloatingPanes(ctx, s.name) +} + +func (s *session) Run(ctx context.Context, name, cwd string, command ...string) error { + return s.client.Run(ctx, s.name, name, cwd, command...) +} + // sessionArgs returns the appropriate session arguments for zellij commands. // If we're inside the target session, returns empty slice (use local actions). // Otherwise returns ["-s", session] to target the specific session. From 1611331cca1e59d1789af45126e29d7a9a97bba1 Mon Sep 17 00:00:00 2001 From: Matthew Newhook Date: Thu, 29 Jan 2026 09:12:43 -0500 Subject: [PATCH 9/9] Remove ClientInterface, update callers to use SessionManager/Session Callers now use m.zj.Session(session).Method() pattern instead of passing session to each method call. Co-Authored-By: Claude Opus 4.5 --- internal/tui/tui_plan.go | 2 +- internal/tui/tui_plan_data.go | 5 +++-- internal/tui/tui_plan_work.go | 2 +- internal/zellij/zellij.go | 36 ++--------------------------------- 4 files changed, 7 insertions(+), 38 deletions(-) diff --git a/internal/tui/tui_plan.go b/internal/tui/tui_plan.go index b7f55267..43867f1b 100644 --- a/internal/tui/tui_plan.go +++ b/internal/tui/tui_plan.go @@ -102,7 +102,7 @@ type planModel struct { // Per-bead session tracking activeBeadSessions map[string]bool // beadID -> has active session - zj zellij.ClientInterface + zj zellij.SessionManager // Two-column layout settings columnRatio float64 // Ratio of issues column width (0.0-1.0), default 0.4 for 40/60 split diff --git a/internal/tui/tui_plan_data.go b/internal/tui/tui_plan_data.go index 3759fff7..31b2570a 100644 --- a/internal/tui/tui_plan_data.go +++ b/internal/tui/tui_plan_data.go @@ -228,7 +228,7 @@ func (m *planModel) closeBead(beadID string) tea.Cmd { // If there's an active session for this bead, close it if m.activeBeadSessions[beadID] { // Terminate and close the tab - _ = m.zj.TerminateAndCloseTab(m.ctx, session, tabName) + _ = m.zj.Session(session).TerminateAndCloseTab(m.ctx, tabName) // Unregister from database _ = m.proj.DB.UnregisterPlanSession(m.ctx, beadID) } @@ -251,11 +251,12 @@ func (m *planModel) closeBeads(beadIDs []string) tea.Cmd { session := m.sessionName() // First, close any active sessions for these beads + zjSession := m.zj.Session(session) for _, beadID := range beadIDs { if m.activeBeadSessions[beadID] { tabName := db.TabNameForBead(beadID) // Terminate and close the tab - _ = m.zj.TerminateAndCloseTab(m.ctx, session, tabName) + _ = zjSession.TerminateAndCloseTab(m.ctx, tabName) // Unregister from database _ = m.proj.DB.UnregisterPlanSession(m.ctx, beadID) } diff --git a/internal/tui/tui_plan_work.go b/internal/tui/tui_plan_work.go index 3981baa2..f4170450 100644 --- a/internal/tui/tui_plan_work.go +++ b/internal/tui/tui_plan_work.go @@ -36,7 +36,7 @@ func (m *planModel) spawnPlanSession(beadID string) tea.Cmd { logging.Debug("spawnPlanSession checked if running", "beadID", beadID, "running", running) if running { // Session exists - just switch to it - if err := m.zj.SwitchToTab(m.ctx, session, tabName); err != nil { + if err := m.zj.Session(session).SwitchToTab(m.ctx, tabName); err != nil { return planSessionSpawnedMsg{beadID: beadID, err: err} } return planSessionSpawnedMsg{beadID: beadID, resumed: true} diff --git a/internal/zellij/zellij.go b/internal/zellij/zellij.go index 99e28228..aec26fa5 100644 --- a/internal/zellij/zellij.go +++ b/internal/zellij/zellij.go @@ -96,37 +96,6 @@ type Session interface { Run(ctx context.Context, name, cwd string, command ...string) error } -// ClientInterface defines the combined interface for zellij operations. -// Deprecated: Use SessionManager and Session interfaces instead. -type ClientInterface interface { - SessionManager - - // Tab management (deprecated - use Session interface) - CreateTab(ctx context.Context, session, name, cwd string) error - CreateTabWithCommand(ctx context.Context, session, name, cwd, command string, args []string, paneName string) error - SwitchToTab(ctx context.Context, session, name string) error - QueryTabNames(ctx context.Context, session string) ([]string, error) - TabExists(ctx context.Context, session, name string) (bool, error) - CloseTab(ctx context.Context, session string) error - - // Pane input control (deprecated - use Session interface) - WriteASCII(ctx context.Context, session string, code int) error - WriteChars(ctx context.Context, session, text string) error - SendCtrlC(ctx context.Context, session string) error - SendEnter(ctx context.Context, session string) error - ExecuteCommand(ctx context.Context, session, cmd string) error - - // High-level operations (deprecated - use Session interface) - TerminateProcess(ctx context.Context, session string) error - ClearAndExecute(ctx context.Context, session, cmd string) error - TerminateAndCloseTab(ctx context.Context, session, tabName string) error - - // Floating pane operations (deprecated - use Session interface) - RunFloating(ctx context.Context, session, name, cwd string, command ...string) error - ToggleFloatingPanes(ctx context.Context, session string) error - Run(ctx context.Context, session, name, cwd string, command ...string) error -} - // Client provides methods for interacting with zellij sessions, tabs, and panes. type Client struct { // Timeouts for various operations @@ -138,9 +107,8 @@ type Client struct { // Compile-time checks. var ( - _ ClientInterface = (*Client)(nil) - _ SessionManager = (*Client)(nil) - _ Session = (*session)(nil) + _ SessionManager = (*Client)(nil) + _ Session = (*session)(nil) ) // New creates a new zellij client with default configuration.