diff --git a/internal/ai/provider/claude_cli.go b/internal/ai/provider/claude_cli.go index 15f9b83c..07b89bf3 100644 --- a/internal/ai/provider/claude_cli.go +++ b/internal/ai/provider/claude_cli.go @@ -137,7 +137,7 @@ func CheckClaudeCLILocalAuth(ctx context.Context) error { defer cancel() args := append(buildClaudeCLILocalAuthIsolationArgs(), "auth", "status", "--json") - cmd := claudeCommandContext(ctx, command.Path, args...) + cmd := newClaudeCLICommand(ctx, command.Path, args...) env, err := buildClaudeCLIEnv(ai.ProviderConfig{AuthMode: "local-cli"}, cmd.Environ(), runtime.GOOS, claudeLookPath, fileExists) if err != nil { return err @@ -224,7 +224,7 @@ func (p *ClaudeCLIProvider) Chat(ctx context.Context, req ai.ChatRequest) (*ai.C if err != nil { return nil, err } - cmd := claudeCommandContext(ctx, command.Path, args...) + cmd := newClaudeCLICommand(ctx, command.Path, args...) if err := p.setEnv(cmd); err != nil { return nil, err } @@ -301,7 +301,7 @@ func (p *ClaudeCLIProvider) ChatStream(ctx context.Context, req ai.ChatRequest, if err != nil { return err } - cmd := claudeCommandContext(ctx, command.Path, args...) + cmd := newClaudeCLICommand(ctx, command.Path, args...) if err := p.setEnv(cmd); err != nil { return err } @@ -469,6 +469,12 @@ func isClaudeCLITimeout(ctx context.Context, err error) bool { return errors.Is(ctx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded) } +func newClaudeCLICommand(ctx context.Context, name string, args ...string) *exec.Cmd { + cmd := claudeCommandContext(ctx, name, args...) + configureClaudeCLICommand(cmd) + return cmd +} + func claudeCLIEndpointForLog(config ai.ProviderConfig) string { baseURL := strings.TrimRight(strings.TrimSpace(config.BaseURL), "/") if baseURL != "" { diff --git a/internal/ai/provider/claude_cli_process_other.go b/internal/ai/provider/claude_cli_process_other.go new file mode 100644 index 00000000..5cdc6431 --- /dev/null +++ b/internal/ai/provider/claude_cli_process_other.go @@ -0,0 +1,7 @@ +//go:build !windows + +package provider + +import "os/exec" + +func configureClaudeCLICommand(_ *exec.Cmd) {} diff --git a/internal/ai/provider/claude_cli_process_windows.go b/internal/ai/provider/claude_cli_process_windows.go new file mode 100644 index 00000000..213898bb --- /dev/null +++ b/internal/ai/provider/claude_cli_process_windows.go @@ -0,0 +1,21 @@ +//go:build windows + +package provider + +import ( + "os/exec" + "syscall" +) + +const claudeCLIWindowsCreateNoWindow uint32 = 0x08000000 + +func configureClaudeCLICommand(cmd *exec.Cmd) { + if cmd == nil { + return + } + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.HideWindow = true + cmd.SysProcAttr.CreationFlags |= claudeCLIWindowsCreateNoWindow +} diff --git a/internal/ai/provider/claude_cli_process_windows_test.go b/internal/ai/provider/claude_cli_process_windows_test.go new file mode 100644 index 00000000..e35d64cd --- /dev/null +++ b/internal/ai/provider/claude_cli_process_windows_test.go @@ -0,0 +1,31 @@ +//go:build windows + +package provider + +import ( + "os/exec" + "syscall" + "testing" +) + +func TestConfigureClaudeCLICommandHidesWindowsConsole(t *testing.T) { + const existingCreationFlag uint32 = 0x00000004 + cmd := &exec.Cmd{ + SysProcAttr: &syscall.SysProcAttr{CreationFlags: existingCreationFlag}, + } + + configureClaudeCLICommand(cmd) + + if cmd.SysProcAttr == nil { + t.Fatal("expected Windows process attributes to be configured") + } + if !cmd.SysProcAttr.HideWindow { + t.Fatal("expected Claude CLI window to be hidden") + } + if cmd.SysProcAttr.CreationFlags&claudeCLIWindowsCreateNoWindow == 0 { + t.Fatalf("expected CREATE_NO_WINDOW, got creation flags %#x", cmd.SysProcAttr.CreationFlags) + } + if cmd.SysProcAttr.CreationFlags&existingCreationFlag == 0 { + t.Fatalf("expected existing creation flags to be preserved, got %#x", cmd.SysProcAttr.CreationFlags) + } +}