From 6a9d55210458d9baa028fb7315dd83b14c76756e Mon Sep 17 00:00:00 2001 From: jcleira Date: Mon, 26 Jan 2026 16:04:58 +0100 Subject: [PATCH] Add claude code integration #01 --- cmd/config/init.go | 4 +- cmd/root.go | 77 +++++++++++++++++++--- pkg/shell/functions.go | 117 ++++++++++++++++++++++------------ pkg/ui/dashboard/dashboard.go | 36 +++++++++-- pkg/ui/dashboard/keys.go | 7 +- 5 files changed, 181 insertions(+), 60 deletions(-) diff --git a/cmd/config/init.go b/cmd/config/init.go index 27279c1..84995ec 100644 --- a/cmd/config/init.go +++ b/cmd/config/init.go @@ -74,6 +74,6 @@ func initShellIntegration() { } fmt.Println() - fmt.Printf("Usage: %s\n", commands.SuccessStyle.Render("w")) - fmt.Printf(" %s\n", commands.SuccessStyle.Render("w ")) + fmt.Printf("Usage: %s (select workspace → Claude Code → loop back)\n", commands.SuccessStyle.Render("w")) + fmt.Printf(" %s (go to workspace → Claude Code → loop back)\n", commands.SuccessStyle.Render("w ")) } diff --git a/cmd/root.go b/cmd/root.go index a727ea6..1d4e830 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,11 +3,12 @@ package cmd import ( "fmt" + "os" + "os/exec" "github.com/spf13/cobra" "github.com/jcleira/workspace/pkg/config" - "github.com/jcleira/workspace/pkg/shell" "github.com/jcleira/workspace/pkg/ui/commands" "github.com/jcleira/workspace/pkg/ui/dashboard" "github.com/jcleira/workspace/pkg/workspace" @@ -72,24 +73,80 @@ func runInteractiveWorkspaceSelector() { return } - selectedPath, err := dashboard.RunDashboard(WorkspaceManager, ConfigManager) - if err != nil { - commands.PrintError(fmt.Sprintf("Dashboard error: %v", err)) + if OutputPathOnly { + runOutputPathOnlyMode() return } - if selectedPath != "" { - if OutputPathOnly { - fmt.Println(selectedPath) + if !isClaudeInstalled() { + commands.PrintError("'claude' command not found") + fmt.Println("Install: https://claude.ai/code") + return + } + + for { + result, err := dashboard.RunDashboard(WorkspaceManager, ConfigManager) + if err != nil { + commands.PrintError(fmt.Sprintf("Dashboard error: %v", err)) + return + } + + if result.Path == "" { + return + } + + if result.LaunchShell { + launchShell(result.Path) } else { - ws := workspace.Workspace{Path: selectedPath} - shell.NavigateToWorkspace(ws) + launchClaude(result.Path) } - } else if OutputPathOnly { + } +} + +func runOutputPathOnlyMode() { + result, err := dashboard.RunDashboard(WorkspaceManager, ConfigManager) + if err != nil { + commands.PrintError(fmt.Sprintf("Dashboard error: %v", err)) + return + } + + if result.Path != "" { + fmt.Println(result.Path) + } else { fmt.Println("quit") } } +func isClaudeInstalled() bool { + _, err := exec.LookPath("claude") + return err == nil +} + +func launchClaude(workspacePath string) { + cmd := exec.Command("claude", "--continue") + cmd.Dir = workspacePath + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + _ = cmd.Run() +} + +func launchShell(workspacePath string) { + shellPath := os.Getenv("SHELL") + if shellPath == "" { + shellPath = "/bin/sh" + } + + cmd := exec.Command(shellPath) + cmd.Dir = workspacePath + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + _ = cmd.Run() +} + // WorkspaceCompletionFunc provides shell completion for workspace names. func WorkspaceCompletionFunc(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { workspaces, err := WorkspaceManager.GetWorkspaces() diff --git a/pkg/shell/functions.go b/pkg/shell/functions.go index 119a7a1..5ec4060 100644 --- a/pkg/shell/functions.go +++ b/pkg/shell/functions.go @@ -3,24 +3,35 @@ package shell // GenerateBashFunction returns a bash shell function for workspace navigation. func GenerateBashFunction() string { - return `# Workspace navigation function + return `# Workspace navigation function with Claude Code integration w() { - if [ $# -eq 0 ]; then - # Interactive selection - local result=$(workspace --output-path-only) - if [ -n "$result" ] && [ "$result" != "quit" ]; then + if ! command -v claude &> /dev/null; then + echo "Error: 'claude' command not found" + echo "Install: https://claude.ai/code" + return 1 + fi + + while true; do + if [ $# -eq 0 ]; then + local result=$(workspace --output-path-only) + if [ -z "$result" ] || [ "$result" = "quit" ]; then + break + fi cd "$result" - fi - else - # Direct workspace navigation - local workspace_path="${HOME}/Tactic/workspaces/workspace-$1" - if [ -d "$workspace_path" ]; then - cd "$workspace_path" else - echo "Workspace '$1' not found" - workspace list + local workspace_path="${HOME}/Tactic/workspaces/workspace-$1" + if [ -d "$workspace_path" ]; then + cd "$workspace_path" + else + echo "Workspace '$1' not found" + workspace list + return 1 + fi + set -- fi - fi + + claude --continue + done } # Completion function for w command @@ -36,24 +47,35 @@ complete -F _w_complete w` // GenerateZshFunction returns a zsh shell function for workspace navigation. func GenerateZshFunction() string { - return `# Workspace navigation function + return `# Workspace navigation function with Claude Code integration w() { - if [ $# -eq 0 ]; then - # Interactive selection - local result=$(workspace --output-path-only) - if [ -n "$result" ] && [ "$result" != "quit" ]; then + if ! command -v claude &> /dev/null; then + echo "Error: 'claude' command not found" + echo "Install: https://claude.ai/code" + return 1 + fi + + while true; do + if [ $# -eq 0 ]; then + local result=$(workspace --output-path-only) + if [ -z "$result" ] || [ "$result" = "quit" ]; then + break + fi cd "$result" - fi - else - # Direct workspace navigation - local workspace_path="${HOME}/Tactic/workspaces/workspace-$1" - if [ -d "$workspace_path" ]; then - cd "$workspace_path" else - echo "Workspace '$1' not found" - workspace list + local workspace_path="${HOME}/Tactic/workspaces/workspace-$1" + if [ -d "$workspace_path" ]; then + cd "$workspace_path" + else + echo "Workspace '$1' not found" + workspace list + return 1 + fi + set -- fi - fi + + claude --continue + done } # Completion function for w command @@ -69,23 +91,34 @@ compdef _w_complete w` // GenerateFishFunction returns a fish shell function for workspace navigation. func GenerateFishFunction() string { - return `# Workspace navigation function -function w --description "Navigate to workspace" - if test (count $argv) -eq 0 - # Interactive selection - set result (workspace --output-path-only) - if test -n "$result" -a "$result" != "quit" + return `# Workspace navigation function with Claude Code integration +function w --description "Navigate to workspace and launch Claude" + if not command -v claude &> /dev/null + echo "Error: 'claude' command not found" + echo "Install: https://claude.ai/code" + return 1 + end + + while true + if test (count $argv) -eq 0 + set result (workspace --output-path-only) + if test -z "$result" -o "$result" = "quit" + break + end cd "$result" - end - else - # Direct workspace navigation - set workspace_path "$HOME/Tactic/workspaces/workspace-$argv[1]" - if test -d "$workspace_path" - cd "$workspace_path" else - echo "Workspace '$argv[1]' not found" - workspace list + set workspace_path "$HOME/Tactic/workspaces/workspace-$argv[1]" + if test -d "$workspace_path" + cd "$workspace_path" + else + echo "Workspace '$argv[1]' not found" + workspace list + return 1 + end + set -e argv end + + claude --continue end end diff --git a/pkg/ui/dashboard/dashboard.go b/pkg/ui/dashboard/dashboard.go index f7b5c3f..c46d5dc 100644 --- a/pkg/ui/dashboard/dashboard.go +++ b/pkg/ui/dashboard/dashboard.go @@ -55,6 +55,7 @@ type DashboardModel struct { statusMessage string selectedPath string + launchShell bool quitting bool } @@ -211,6 +212,16 @@ func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil + case key.Matches(msg, m.keys.Shell): + ws := m.workspaceList.SelectedWorkspace() + if ws != nil { + m.selectedPath = ws.Path + m.launchShell = true + m.quitting = true + return m, tea.Quit + } + return m, nil + case key.Matches(msg, m.keys.Refresh): return m, m.refreshSelectedStatus() @@ -552,7 +563,8 @@ func (m DashboardModel) renderFooter() string { keys := []string{ helpKeyStyle.Render("↑/↓ j/k") + helpDescStyle.Render(" navigate"), helpKeyStyle.Render("←/→ h/l") + helpDescStyle.Render(" panels"), - helpKeyStyle.Render("Enter") + helpDescStyle.Render(" select"), + helpKeyStyle.Render("Enter") + helpDescStyle.Render(" claude"), + helpKeyStyle.Render("s") + helpDescStyle.Render(" shell"), helpKeyStyle.Render("f") + helpDescStyle.Render(" fetch"), helpKeyStyle.Render("p") + helpDescStyle.Render(" pull"), helpKeyStyle.Render("G") + helpDescStyle.Render(" staged diff"), @@ -578,16 +590,30 @@ func (m DashboardModel) SelectedPath() string { return m.selectedPath } -// RunDashboard runs the dashboard and returns the selected workspace path. -func RunDashboard(wm *workspace.Manager, cm *config.ConfigManager) (string, error) { +// LaunchShell returns true if the user wants to launch a shell instead of Claude. +func (m DashboardModel) LaunchShell() bool { + return m.launchShell +} + +// DashboardResult contains the result of running the dashboard. +type DashboardResult struct { + Path string + LaunchShell bool +} + +// RunDashboard runs the dashboard and returns the selected workspace path and action. +func RunDashboard(wm *workspace.Manager, cm *config.ConfigManager) (DashboardResult, error) { m := NewDashboard(wm, cm) p := tea.NewProgram(m, tea.WithAltScreen()) finalModel, err := p.Run() if err != nil { - return "", err + return DashboardResult{}, err } dm := finalModel.(DashboardModel) - return dm.SelectedPath(), nil + return DashboardResult{ + Path: dm.SelectedPath(), + LaunchShell: dm.LaunchShell(), + }, nil } diff --git a/pkg/ui/dashboard/keys.go b/pkg/ui/dashboard/keys.go index d8587d3..0120569 100644 --- a/pkg/ui/dashboard/keys.go +++ b/pkg/ui/dashboard/keys.go @@ -9,6 +9,7 @@ type KeyMap struct { Left key.Binding Right key.Binding Select key.Binding + Shell key.Binding Fetch key.Binding Pull key.Binding Delete key.Binding @@ -44,7 +45,11 @@ func DefaultKeyMap() KeyMap { ), Select: key.NewBinding( key.WithKeys("enter"), - key.WithHelp("enter", "switch workspace"), + key.WithHelp("enter", "claude"), + ), + Shell: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "shell"), ), Fetch: key.NewBinding( key.WithKeys("f"),