Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ private async Task<PersistentShellCommandResult> RunCommandInternalAsync(string
isSyntaxError
);
}
catch (OperationCanceledException ex)
catch (OperationCanceledException)
{
var duration = DateTime.Now - startTime;
var looksLikeTimeout = timeoutMs.HasValue && Math.Abs(duration.TotalMilliseconds - timeoutMs.Value) < 100;
Expand Down Expand Up @@ -467,7 +467,7 @@ protected virtual async Task<RunnableProcessResult> WaitForMarkerAsync(Cancellat

Logger.Warning($"🔓 WaitForMarkerAsync: OUTSIDE LOCK - About to check regex - Thread={Thread.CurrentThread.ManagedThreadId}");

if (regex.IsMatch(currentOutput))
if (regex.IsMatch(currentOutput!))
{
Logger.Warning($"✅ WaitForMarkerAsync: MARKER FOUND! About to read error/merged outputs OUTSIDE LOCK - Thread={Thread.CurrentThread.ManagedThreadId}, Time={DateTime.Now:HH:mm:ss.fff}");

Expand All @@ -478,13 +478,13 @@ protected virtual async Task<RunnableProcessResult> WaitForMarkerAsync(Cancellat
Logger.Warning($"🔓 WaitForMarkerAsync: GetCurrentMergedOutput() returned length={merged?.Length ?? 0} - Thread={Thread.CurrentThread.ManagedThreadId}");

// Compare with what we captured earlier
Logger.Warning($"📊 WaitForMarkerAsync: COMPARISON - currentOutput.Length={currentOutput.Length}, merged.Length={merged?.Length ?? 0}, match={currentOutput.Length == (merged?.Length ?? 0)}");
Logger.Warning($"📊 WaitForMarkerAsync: COMPARISON - currentOutput.Length={currentOutput!.Length}, merged.Length={merged?.Length ?? 0}, match={currentOutput.Length == (merged?.Length ?? 0)}");

// Marker found, return the current output
return new RunnableProcessResult(
currentOutput,
error,
merged,
error ?? string.Empty,
merged ?? string.Empty,
0, // Actual exit code will be parsed later
ProcessCompletionState.Completed,
DateTime.Now - startTime
Expand Down
6 changes: 3 additions & 3 deletions src/cycod/CommandLineCommands/ChatCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,11 @@ public override async Task<object> ExecuteAsync(bool interactive)
break;
}

var (skipAssistant, replaceUserPrompt) = await TryHandleChatCommandAsync(chat, userPrompt);
var (skipAssistant, replaceUserPrompt) = await TryHandleChatCommandAsync(chat, userPrompt!);
if (skipAssistant) continue; // Some chat commands don't require a response from the assistant.

var shouldReplaceUserPrompt = !string.IsNullOrEmpty(replaceUserPrompt);
if (shouldReplaceUserPrompt) DisplayPromptReplacement(userPrompt, replaceUserPrompt);
if (shouldReplaceUserPrompt) DisplayPromptReplacement(userPrompt!, replaceUserPrompt);

var giveAssistant = shouldReplaceUserPrompt ? replaceUserPrompt! : userPrompt;

Expand All @@ -210,7 +210,7 @@ public override async Task<object> ExecuteAsync(bool interactive)
var imageFiles = ImagePatterns.Any() ? ImageResolver.ResolveImagePatterns(ImagePatterns) : new List<string>();
ImagePatterns.Clear();

var response = await CompleteChatStreamingAsync(chat, giveAssistant, imageFiles,
var response = await CompleteChatStreamingAsync(chat, giveAssistant!, imageFiles,
(messages) => HandleUpdateMessages(messages),
(update) => HandleStreamingChatCompletionUpdate(update),
(name, args) => HandleFunctionCallApproval(factory, name, args!),
Expand Down
156 changes: 152 additions & 4 deletions src/cycod/FunctionCallingTools/ScreenshotHelperFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ public ScreenshotHelperFunctions(ChatCommand chatCommand)
_chatCommand = chatCommand;
}

#if WINDOWS
[Description("Take a screenshot of the primary screen and add it to the conversation. The screenshot will be included in the next message exchange. Only works on Windows.")]
#if WINDOWS || OSX
[Description("Take a screenshot of the primary screen and add it to the conversation. The screenshot will be included in the next message exchange. Works on Windows and macOS.")]
public object TakeScreenshot()
{
// Check platform support
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return ScreenshotHelper.GetPlatformErrorMessage();
}
Expand All @@ -27,7 +27,13 @@ public object TakeScreenshot()
// Capture screenshot
var filePath = ScreenshotHelper.TakeScreenshot();
var fileExists = FileHelpers.FileExists(filePath);
if (!fileExists) return "Failed to capture screenshot. Please check that the display is accessible.";
if (!fileExists)
{
// Use macOS-specific error message if on macOS, otherwise generic message
return RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? ScreenshotHelper.GetMacOSPermissionErrorMessage()
: "Failed to capture screenshot. Please check that the display is accessible.";
}

// Load the screenshot and return as DataContent for immediate inclusion
try
Expand All @@ -49,5 +55,147 @@ public object TakeScreenshot()
}
#endif

#if OSX
[Description("Take a screenshot of a window with matching title (partial match, case-insensitive). Returns the screenshot file path or an error message. Example: title='cycod' or title='Microsoft Edge'.")]
public object TakeScreenshotOfWindowWithTitle(string title)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Window-specific screenshots are only available on macOS.";
}

try
{
var result = ScreenshotHelper.TakeScreenshotOfWindowWithTitle(title);

// If result is an existing file path, load and return as DataContent
if (!string.IsNullOrEmpty(result) && File.Exists(result))
{
var imageBytes = File.ReadAllBytes(result);
var mediaType = ImageResolver.GetMediaTypeFromFileExtension(result);
return new DataContent(imageBytes, mediaType);
}

// Otherwise it's an error message
return result;
}
catch (Exception ex)
{
return $"Error capturing screenshot by title: {ex.Message}";
}
}

[Description("Take a screenshot of a window from the specified application (partial match, case-insensitive). Returns the screenshot file path or an error message. Example: appName='Warp' or appName='Code'.")]
public object TakeScreenshotOfApp(string appName)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Window-specific screenshots are only available on macOS.";
}

try
{
var result = ScreenshotHelper.TakeScreenshotOfApp(appName);

// If result starts with '/', it's a file path - load and return as DataContent
if (result.StartsWith('/') || result.StartsWith(Path.GetTempPath()))
{
var imageBytes = File.ReadAllBytes(result);
var mediaType = ImageResolver.GetMediaTypeFromFileExtension(result);
return new DataContent(imageBytes, mediaType);
}

// Otherwise it's an error message
return result;
}
catch (Exception ex)
{
return $"Error capturing screenshot by app: {ex.Message}";
}
}

[Description("Take a screenshot of a specific display. displayNumber: 1 for main display, 2 for secondary, etc. Returns the screenshot or an error message.")]
public object TakeScreenshotOfDisplay(int displayNumber)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Display-specific screenshots are only available on macOS.";
}

try
{
var filePath = ScreenshotHelper.TakeScreenshotOfDisplay(displayNumber);
if (filePath == null)
{
return $"Failed to capture screenshot of display {displayNumber}. Please check that the display exists.";
}

var imageBytes = File.ReadAllBytes(filePath);
var mediaType = ImageResolver.GetMediaTypeFromFileExtension(filePath);
return new DataContent(imageBytes, mediaType);
}
catch (Exception ex)
{
return $"Error capturing screenshot of display {displayNumber}: {ex.Message}";
}
}

[Description("List all visible application windows with their metadata (window ID, app name, title, position, size). Returns a JSON array of window information. Useful for finding windows before capturing them.")]
public object ListWindows()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Window enumeration is only available on macOS.";
}

try
{
var windows = ScreenshotHelper.EnumerateWindows();

if (windows.Count == 0)
{
return "No application windows found.";
}

var json = System.Text.Json.JsonSerializer.Serialize(windows, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});

return json;
}
catch (Exception ex)
{
return $"Error listing windows: {ex.Message}";
}
}

[Description("Take a screenshot of a specific window by ID. Use ListWindows() first to get window IDs. Returns the screenshot or an error message. This is an advanced method - prefer TakeScreenshotOfWindowWithTitle or TakeScreenshotOfApp for simpler use.")]
public object TakeScreenshotOfWindow(int windowId)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Window-specific screenshots are only available on macOS.";
}

try
{
var filePath = ScreenshotHelper.TakeScreenshotOfWindow(windowId);
if (filePath == null)
{
return $"Failed to capture screenshot of window {windowId}. Please check that the window ID is valid.";
}

var imageBytes = File.ReadAllBytes(filePath);
var mediaType = ImageResolver.GetMediaTypeFromFileExtension(filePath);
return new DataContent(imageBytes, mediaType);
}
catch (Exception ex)
{
return $"Error capturing screenshot of window {windowId}: {ex.Message}";
}
}
#endif

private readonly ChatCommand _chatCommand;
}
Loading