diff --git a/.lastmerge b/.lastmerge index 04846a767..2e1ed67d3 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -304d812cd4c98755159da427c6701bfb7e0b7c32 +5016587a62652f3d184b3c6958dfc63359921aa8 diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 1ac43c719..f4f570d49 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -85,6 +85,13 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { var pb = new ProcessBuilder(command); pb.redirectErrorStream(false); + // Note: On Windows, console window visibility depends on how the parent Java + // process was launched. GUI applications started with 'javaw' will not create + // visible console windows for subprocesses. Console applications started with + // 'java' will share their console with subprocesses. Java's ProcessBuilder + // doesn't provide explicit CREATE_NO_WINDOW flags like native Windows APIs, + // but the default behavior is appropriate for most use cases. + if (options.getCwd() != null) { pb.directory(new File(options.getCwd())); } diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index b51ab7a19..d83b8b6eb 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -323,4 +323,32 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) { this.useLoggedInUser = useLoggedInUser; return this; } + + /** + * Creates a shallow clone of this {@code CopilotClientOptions} instance. + *

+ * Array properties (like {@code cliArgs}) are copied into new arrays so that + * modifications to the clone do not affect the original. The + * {@code environment} map is also copied to a new map instance. Other + * reference-type properties are shared between the original and clone. + * + * @return a clone of this options instance + */ + @Override + public CopilotClientOptions clone() { + CopilotClientOptions copy = new CopilotClientOptions(); + copy.cliPath = this.cliPath; + copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null; + copy.cwd = this.cwd; + copy.port = this.port; + copy.useStdio = this.useStdio; + copy.cliUrl = this.cliUrl; + copy.logLevel = this.logLevel; + copy.autoStart = this.autoStart; + copy.autoRestart = this.autoRestart; + copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; + copy.githubToken = this.githubToken; + copy.useLoggedInUser = this.useLoggedInUser; + return copy; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java index 99c4214b8..6ba629b7b 100644 --- a/src/main/java/com/github/copilot/sdk/json/MessageOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/MessageOptions.java @@ -4,6 +4,7 @@ package com.github.copilot.sdk.json; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -107,4 +108,23 @@ public String getMode() { return mode; } + /** + * Creates a shallow clone of this {@code MessageOptions} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like attachment items) are not + * deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this options instance + */ + @Override + public MessageOptions clone() { + MessageOptions copy = new MessageOptions(); + copy.prompt = this.prompt; + copy.attachments = this.attachments != null ? new ArrayList<>(this.attachments) : null; + copy.mode = this.mode; + return copy; + } + } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index 8b7d841f8..fc790258a 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -4,6 +4,7 @@ package com.github.copilot.sdk.json; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -475,4 +476,40 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes this.infiniteSessions = infiniteSessions; return this; } + + /** + * Creates a shallow clone of this {@code ResumeSessionConfig} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like provider configuration, + * system messages, hooks, infinite session configuration, and handlers) are not + * deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this config instance + */ + @Override + public ResumeSessionConfig clone() { + ResumeSessionConfig copy = new ResumeSessionConfig(); + copy.model = this.model; + copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; + copy.systemMessage = this.systemMessage; + copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; + copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; + copy.provider = this.provider; + copy.reasoningEffort = this.reasoningEffort; + copy.onPermissionRequest = this.onPermissionRequest; + copy.onUserInputRequest = this.onUserInputRequest; + copy.hooks = this.hooks; + copy.workingDirectory = this.workingDirectory; + copy.configDir = this.configDir; + copy.disableResume = this.disableResume; + copy.streaming = this.streaming; + copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.infiniteSessions = this.infiniteSessions; + return copy; + } } diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 9d7e7367f..064fee9f3 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -4,6 +4,7 @@ package com.github.copilot.sdk.json; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -512,4 +513,40 @@ public SessionConfig setConfigDir(String configDir) { this.configDir = configDir; return this; } + + /** + * Creates a shallow clone of this {@code SessionConfig} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like provider configuration, + * system messages, hooks, infinite session configuration, and handlers) are not + * deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this config instance + */ + @Override + public SessionConfig clone() { + SessionConfig copy = new SessionConfig(); + copy.sessionId = this.sessionId; + copy.model = this.model; + copy.reasoningEffort = this.reasoningEffort; + copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; + copy.systemMessage = this.systemMessage; + copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; + copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; + copy.provider = this.provider; + copy.onPermissionRequest = this.onPermissionRequest; + copy.onUserInputRequest = this.onUserInputRequest; + copy.hooks = this.hooks; + copy.workingDirectory = this.workingDirectory; + copy.streaming = this.streaming; + copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.infiniteSessions = this.infiniteSessions; + copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.configDir = this.configDir; + return copy; + } } diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java new file mode 100644 index 000000000..3bd1b2344 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.MessageOptions; + +class ConfigCloneTest { + + @Test + void copilotClientOptionsCloneBasic() { + CopilotClientOptions original = new CopilotClientOptions(); + original.setCliPath("/usr/local/bin/copilot"); + original.setLogLevel("debug"); + original.setPort(9000); + + CopilotClientOptions cloned = original.clone(); + + assertEquals(original.getCliPath(), cloned.getCliPath()); + assertEquals(original.getLogLevel(), cloned.getLogLevel()); + assertEquals(original.getPort(), cloned.getPort()); + } + + @Test + void copilotClientOptionsArrayIndependence() { + CopilotClientOptions original = new CopilotClientOptions(); + String[] args = {"--flag1", "--flag2"}; + original.setCliArgs(args); + + CopilotClientOptions cloned = original.clone(); + cloned.getCliArgs()[0] = "--changed"; + + assertEquals("--flag1", original.getCliArgs()[0]); + assertEquals("--changed", cloned.getCliArgs()[0]); + } + + @Test + void copilotClientOptionsEnvironmentIndependence() { + CopilotClientOptions original = new CopilotClientOptions(); + Map env = new HashMap<>(); + env.put("KEY1", "value1"); + original.setEnvironment(env); + + CopilotClientOptions cloned = original.clone(); + + // Mutate the original environment map to test independence + env.put("KEY2", "value2"); + + // The cloned config should be unaffected by mutations to the original map + assertEquals(1, cloned.getEnvironment().size()); + assertEquals(2, original.getEnvironment().size()); + } + + @Test + void sessionConfigCloneBasic() { + SessionConfig original = new SessionConfig(); + original.setSessionId("my-session"); + original.setModel("gpt-4o"); + original.setStreaming(true); + + SessionConfig cloned = original.clone(); + + assertEquals(original.getSessionId(), cloned.getSessionId()); + assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.isStreaming(), cloned.isStreaming()); + } + + @Test + void sessionConfigListIndependence() { + SessionConfig original = new SessionConfig(); + List toolList = new ArrayList<>(); + toolList.add("grep"); + toolList.add("bash"); + original.setAvailableTools(toolList); + + SessionConfig cloned = original.clone(); + + // Mutate the original list directly to test independence + toolList.add("web"); + + // The cloned config should be unaffected by mutations to the original list + assertEquals(2, cloned.getAvailableTools().size()); + assertEquals(3, original.getAvailableTools().size()); + } + + @Test + void resumeSessionConfigCloneBasic() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setModel("o1"); + original.setStreaming(false); + + ResumeSessionConfig cloned = original.clone(); + + assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.isStreaming(), cloned.isStreaming()); + } + + @Test + void messageOptionsCloneBasic() { + MessageOptions original = new MessageOptions(); + original.setPrompt("What is 2+2?"); + original.setMode("immediate"); + + MessageOptions cloned = original.clone(); + + assertEquals(original.getPrompt(), cloned.getPrompt()); + assertEquals(original.getMode(), cloned.getMode()); + } + + @Test + void clonePreservesNullFields() { + CopilotClientOptions opts = new CopilotClientOptions(); + CopilotClientOptions optsClone = opts.clone(); + assertNull(optsClone.getCliPath()); + + SessionConfig cfg = new SessionConfig(); + SessionConfig cfgClone = cfg.clone(); + assertNull(cfgClone.getModel()); + + MessageOptions msg = new MessageOptions(); + MessageOptions msgClone = msg.clone(); + assertNull(msgClone.getMode()); + } +}