Skip to content
Open
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
67 changes: 67 additions & 0 deletions Tools/windows/uia-live-gate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ $oldFixture = [Environment]::GetEnvironmentVariable("GRAPHCODE_UIA_FIXTURE_ROWS"
$oldDaemonPipe = [Environment]::GetEnvironmentVariable("GRAPHCODE_DAEMON_PIPE")
$oldSupportDirectory = [Environment]::GetEnvironmentVariable("GRAPHCODE_SUPPORT_DIR")
$oldResetSidebar = [Environment]::GetEnvironmentVariable("GRAPHCODE_UIA_RESET_SIDEBAR")
$oldUpdateAvailable = [Environment]::GetEnvironmentVariable("GRAPHCODE_UIA_UPDATE_AVAILABLE")
$oldShowUpdate = [Environment]::GetEnvironmentVariable("GRAPHCODE_UIA_SHOW_UPDATE")
$process = $null
$settingsProcess = $null
$status = $null
Expand All @@ -305,6 +307,8 @@ try {
$env:GRAPHCODE_GATE_CWD = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$env:GRAPHCODE_UIA_GATE = "1"
$env:GRAPHCODE_UIA_CONNECTION_FAILURE = "1"
$env:GRAPHCODE_UIA_UPDATE_AVAILABLE = "1"
$env:GRAPHCODE_UIA_SHOW_UPDATE = "1"
$env:USERNAME = "GraphCodeUIAGate"
$env:GRAPHCODE_UIA_FIXTURE_ROWS = "C:\fixture-safe|safe,C:\fixture-unsafe|unsafe"
$env:GRAPHCODE_DAEMON_PIPE = "\\.\pipe\graphcode-uia-gate-$PID"
Expand Down Expand Up @@ -353,6 +357,50 @@ try {
$controlWalker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$shellWindow = $process.MainWindowHandle
$desktop = [System.Windows.Automation.AutomationElement]::RootElement
$updateDialog = $desktop.FindFirst(
[System.Windows.Automation.TreeScope]::Descendants,
(New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::NameProperty,
"GraphCode Update Available"
))
)
Require ($null -ne $updateDialog) "update offer dialog did not appear"
Start-Sleep -Milliseconds 250
$updateDialog = $desktop.FindFirst(
[System.Windows.Automation.TreeScope]::Descendants,
(New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::NameProperty,
"GraphCode Update Available"
))
)
$installButton = $updateDialog.FindFirst(
[System.Windows.Automation.TreeScope]::Descendants,
(New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::NameProperty,
"Install"
))
)
$releaseNotesButton = $updateDialog.FindFirst(
[System.Windows.Automation.TreeScope]::Descendants,
(New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::NameProperty,
"Release Notes"
))
)
$laterButton = $updateDialog.FindFirst(
[System.Windows.Automation.TreeScope]::Descendants,
(New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::NameProperty,
"Later"
))
)
Require (($null -ne $installButton) -and (-not $installButton.Current.IsEnabled)) `
"update offer did not expose a disabled Install action"
Require (($null -ne $releaseNotesButton) -and ($null -ne $laterButton)) `
"update offer did not expose Release Notes and Later actions"
Require ([GraphCodeUiaGateState]::SendCommand([IntPtr]$updateDialog.Current.NativeWindowHandle, 9703)) `
"update offer Later action could not be invoked"
Start-Sleep -Milliseconds 150
$status = Find-FragmentById $root "status" $controlWalker
$rawRootChildren = @(Assert-FragmentLinks $root $rawWalker $expectedRootIds "RawView root")
$controlRootChildren = @(Assert-FragmentLinks $root $controlWalker $expectedRootIds "ControlView root")
Expand Down Expand Up @@ -556,6 +604,10 @@ try {
})
Require (($quickChatCards.Count -eq 2) -and
((@($quickChatCards | ForEach-Object { $_.Current.Name }) -join "|") -eq "UIA chat A|UIA chat B")) "Quick Chats did not expose synchronized cards: $(@($quickChatCards | ForEach-Object { $_.Current.Name }) -join '|')"
$surfaceActionPatterns["canvas-primary-action"].Invoke()
Start-Sleep -Milliseconds 150
Require ((Find-FragmentById $root "status" $rawWalker).Current.Name -eq "Creating quick chat...") `
"Populated Quick Chats canvas omitted its New Chat action"
$quickChatRows = @(Get-DirectChildren $projects $rawWalker | Where-Object {
$_.Current.AutomationId -match '^quick-chat-row-'
})
Expand Down Expand Up @@ -603,6 +655,15 @@ try {
Start-Sleep -Milliseconds 150
Require ((Find-FragmentById $root "status" $rawWalker).Current.Name -eq "Opening quick chat...") `
"Quick Chat invocation did not perform its expected action"
$graph = Find-FragmentById $root "graph" $rawWalker
$quickChatWorkspace = @(Get-DirectChildren $graph $rawWalker | Where-Object {
$_.Current.AutomationId -match '^quick-chat-workspace-' -and
$_.Current.Name -eq "Quick Chat terminal workspace"
}) | Select-Object -First 1
Require ($null -ne $quickChatWorkspace) "Quick Chat invocation did not expose its terminal workspace"
Require (($quickChatWorkspace.Current.BoundingRectangle.Width -gt 0) -and
($quickChatWorkspace.Current.BoundingRectangle.Height -gt 0)) `
"Quick Chat terminal workspace has empty bounds"
$surfaceActionPatterns["zoom-in"].Invoke()
$surfaceActionPatterns["actual-size"].Invoke()
$surfaceActionPatterns["zoom-out"].Invoke()
Expand Down Expand Up @@ -1750,4 +1811,10 @@ try {
if ($null -eq $oldResetSidebar) {
Remove-Item Env:GRAPHCODE_UIA_RESET_SIDEBAR -ErrorAction SilentlyContinue
} else { $env:GRAPHCODE_UIA_RESET_SIDEBAR = $oldResetSidebar }
if ($null -eq $oldUpdateAvailable) {
Remove-Item Env:GRAPHCODE_UIA_UPDATE_AVAILABLE -ErrorAction SilentlyContinue
} else { $env:GRAPHCODE_UIA_UPDATE_AVAILABLE = $oldUpdateAvailable }
if ($null -eq $oldShowUpdate) {
Remove-Item Env:GRAPHCODE_UIA_SHOW_UPDATE -ErrorAction SilentlyContinue
} else { $env:GRAPHCODE_UIA_SHOW_UPDATE = $oldShowUpdate }
}
1 change: 1 addition & 0 deletions graphcode-windows/src/AccessibilityProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ class Node final : public IRawElementProviderSimple,
row.identity.rfind("quick-chat-new:", 0) == 0 ? L"quick-chat-new-" :
row.identity.rfind("quick-chats-disclosure:", 0) == 0 ? L"quick-chats-disclosure-" :
row.identity.rfind("quick-chat-row:", 0) == 0 ? L"quick-chat-row-" :
row.identity.rfind("quick-chat-workspace:", 0) == 0 ? L"quick-chat-workspace-" :
row.identity.rfind("loop-disclosure:", 0) == 0 ? L"loop-disclosure-" :
parent == 1 ? L"project-row-" :
parent == 2 ? L"loop-row-" :
Expand Down
143 changes: 108 additions & 35 deletions graphcode-windows/src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ProductSettings = @import("WindowsProductSettings.zig");
const RepositoryDialogs = @import("WindowsRepositoryDialogs.zig");
const Onboarding = @import("WindowsOnboarding.zig");
const WindowsUpdates = @import("WindowsUpdates.zig");
const UpdateOfferDialog = @import("UpdateOfferDialog.zig");
const WorktreeDialog = @import("WorktreeDialog.zig");
const Accessibility = @import("Accessibility.zig");
const Navigation = @import("Navigation.zig");
Expand Down Expand Up @@ -412,6 +413,7 @@ pub const App = struct {
if (self.workspace) |workspace| try workspace.startInputWorker();
}
self.layoutWorkspace();
if (envFlag("GRAPHCODE_UIA_SHOW_UPDATE")) self.showCurrentUpdateOffer();
if (!envFlag("GRAPHCODE_UIA_UPDATE_AVAILABLE")) self.requestUpdateCheck(false);
if (std.process.getEnvVarOwned(self.allocator, "GRAPHCODE_SHELL_REQUIRE_DAEMON")) |value| {
defer self.allocator.free(value);
Expand Down Expand Up @@ -931,6 +933,7 @@ pub const App = struct {
self.setStatus("Unable to open quick chat workspace");
};
}
self.syncAccessibility();
return;
}
}
Expand Down Expand Up @@ -1318,40 +1321,35 @@ pub const App = struct {
self.setStatus("Update release URL is not a trusted GraphCode release page");
return;
};
const message = std.fmt.allocPrint(
const action = UpdateOfferDialog.show(
self.window.hwnd,
self.allocator,
"GraphCode {s} is available.\n\nOpen the verified GitHub release page to review release notes?\n\nIn-app Windows installation and relaunch are not available. Release assets may target other platforms.",
.{if (version.len == 0) "update" else version},
version,
"In-app Windows installation is unavailable until a published, signed Windows artifact exists.",
) catch {
self.setStatus("Unable to prepare the update offer");
return;
};
defer self.allocator.free(message);
const message_wide = std.unicode.utf8ToUtf16LeAllocZ(self.allocator, message) catch {
self.setStatus("Unable to encode the update offer");
return;
};
defer self.allocator.free(message_wide);
if (c.MessageBoxW(
self.window.hwnd,
message_wide.ptr,
std.unicode.utf8ToUtf16LeStringLiteral("GraphCode Update Available").ptr,
c.MB_ICONINFORMATION | c.MB_YESNO | c.MB_DEFBUTTON1,
) != c.IDYES) return;
const url_wide = std.unicode.utf8ToUtf16LeAllocZ(self.allocator, url) catch {
self.setStatus("Unable to encode the release URL");
return;
};
defer self.allocator.free(url_wide);
const result = c.ShellExecuteW(
self.window.hwnd,
std.unicode.utf8ToUtf16LeStringLiteral("open").ptr,
url_wide.ptr,
null,
null,
c.SW_SHOWNORMAL,
);
self.setStatus(if (@intFromPtr(result) <= 32) "Unable to open the release page" else "Opened the GraphCode release page");
switch (action) {
.later => self.setStatus("Update offer deferred"),
.install_unavailable => self.setStatus("Install is unavailable until a signed Windows artifact is published"),
.release_notes => {
const url_wide = std.unicode.utf8ToUtf16LeAllocZ(self.allocator, url) catch {
self.setStatus("Unable to encode the release URL");
return;
};
defer self.allocator.free(url_wide);
const result = c.ShellExecuteW(
self.window.hwnd,
std.unicode.utf8ToUtf16LeStringLiteral("open").ptr,
url_wide.ptr,
null,
null,
c.SW_SHOWNORMAL,
);
self.setStatus(if (@intFromPtr(result) <= 32) "Unable to open the release page" else "Opened the GraphCode release page");
},
}
}

fn showCurrentUpdateOffer(self: *App) void {
Expand Down Expand Up @@ -1746,6 +1744,18 @@ pub const App = struct {
self.client.sendForgetProject(stable.path);
self.setStatus("Removing project from GraphCode...");
},
.trash_project => {
if (stable.remote) return;
if (!GraphContextMenu.confirm(
self.window.hwnd,
"Move Project to Recycle Bin",
"Move this project folder to the Windows Recycle Bin?\n\nIts files will be removed from the filesystem but can be restored from the Recycle Bin.",
)) return;
if (self.moveProjectToRecycleBin(stable.path)) {
self.client.sendForgetProject(stable.path);
self.setStatus("Moved project to the Recycle Bin");
}
},
.delete_project_loops => {
self.deleteProjectLoops(stable.path);
},
Expand Down Expand Up @@ -1873,6 +1883,33 @@ pub const App = struct {
self.setStatus(if (@intFromPtr(result) <= 32) "Unable to open Explorer" else "Opened project in Explorer");
}

fn moveProjectToRecycleBin(self: *App, path: []const u8) bool {
const raw = std.unicode.utf8ToUtf16LeAlloc(self.allocator, path) catch {
self.setStatus("Unable to encode project path for the Recycle Bin");
return false;
};
defer self.allocator.free(raw);
const wide = self.allocator.alloc(u16, raw.len + 2) catch {
self.setStatus("Unable to prepare the Recycle Bin operation");
return false;
};
defer self.allocator.free(wide);
@memcpy(wide[0..raw.len], raw);
wide[raw.len] = 0;
wide[raw.len + 1] = 0;
var operation: c.SHFILEOPSTRUCTW = std.mem.zeroes(c.SHFILEOPSTRUCTW);
operation.hwnd = self.window.hwnd;
operation.wFunc = c.FO_DELETE;
operation.pFrom = wide.ptr;
operation.fFlags = c.FOF_ALLOWUNDO | c.FOF_NOCONFIRMATION | c.FOF_SILENT;
const result = c.SHFileOperationW(&operation);
if (result != 0 or operation.fAnyOperationsAborted != 0) {
self.setStatus("Unable to move the project to the Recycle Bin");
return false;
}
return true;
}

fn showRemoteProjectInfo(self: *App, path: []const u8) void {
const message = std.fmt.allocPrint(
self.allocator,
Expand Down Expand Up @@ -3081,6 +3118,34 @@ pub const App = struct {
else => {},
}
}
if (self.surface == .workspace) if (self.selected_quick_chat) |chat_index| {
if (chat_index < self.model.quick_chats.items.len) {
const chat = self.model.quick_chats.items[chat_index];
const identity = std.fmt.allocPrint(self.allocator, "quick-chat-workspace:{s}", .{chat.id}) catch return;
owned_identities.append(identity) catch {
self.allocator.free(identity);
return;
};
const workspace_bounds = c.RECT{
.left = canvas_rect.left,
.top = inputBounds(client.right, client.bottom, self.workspace_controls).workspace_top,
.right = canvas_rect.right,
.bottom = client.bottom,
};
elements.append(.{
.identity = identity,
.name = "Quick Chat terminal workspace",
.parent = 4,
.selected = true,
.eligible = false,
.invokable = false,
.left = workspace_bounds.left,
.top = workspace_bounds.top,
.right = workspace_bounds.right,
.bottom = workspace_bounds.bottom,
}) catch return;
}
};
switch (self.surface) {
.project, .workspace => if (self.model.graph) |graph| {
if (self.model.open_composite_id) |parent_id| {
Expand Down Expand Up @@ -3149,11 +3214,6 @@ pub const App = struct {
.bottom = bounds.bottom,
}) catch return;
}
if (self.worktree_dialog == null) if (std.process.getEnvVarOwned(self.allocator, "GRAPHCODE_UIA_FIXTURE_ROWS") catch null) |fixture| {
defer self.allocator.free(fixture);
provider.syncStatus(self.status());
return;
};
const policy = if (self.worktree_dialog) |dialog| dialog.policy else WorktreeStatus.Policy{};
provider.syncElements(self.status(), elements.items, policy);
}
Expand Down Expand Up @@ -3324,8 +3384,21 @@ pub const App = struct {
},
.composite_back => self.closeCompositeGroup(),
.quick_chat => |id| {
self.client.sendOpenQuickChat(id);
self.setStatus("Opening quick chat...");
if (envFlag("GRAPHCODE_UIA_GATE")) {
for (self.model.quick_chats.items, 0..) |chat, index| {
if (!std.mem.eql(u8, chat.id, id)) continue;
self.selected_quick_chat = index;
self.surface = .workspace;
self.workspace_controls.panel_visible = true;
self.layoutWorkspace();
self.layoutEmptyStateControls();
self.syncAccessibility();
break;
}
} else {
self.client.sendOpenQuickChat(id);
}
},
}
self.clampSidebarScroll();
Expand Down
8 changes: 7 additions & 1 deletion graphcode-windows/src/GraphContextMenu.zig
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub const Action = enum {
remote_project_info,
close_project,
remove_project,
trash_project,
delete_project_loops,
new_quick_chat,
};
Expand All @@ -66,7 +67,7 @@ pub const Callback = *const fn (?*anyopaque, Action, Target) void;

pub fn requiresConfirmation(action: Action) bool {
return action == .delete_node or action == .delete_edge or action == .delete_quick_chat or
action == .remove_project or action == .delete_project_loops;
action == .remove_project or action == .trash_project or action == .delete_project_loops;
}

pub fn shouldApply(action: Action, confirmed: bool) bool {
Expand Down Expand Up @@ -103,6 +104,7 @@ const ids = struct {
const remote_project_info = 5145;
const close_project = 5146;
const remove_project = 5147;
const trash_project = 5149;
const delete_project_loops = 5148;
const new_quick_chat = 5150;
};
Expand Down Expand Up @@ -133,6 +135,7 @@ pub fn show(
separator(menu);
append(menu, ids.close_project, "Close Project");
append(menu, ids.remove_project, "Remove from GraphCode...");
if (!project.remote) append(menu, ids.trash_project, "Move Folder to Recycle Bin...");
append(menu, ids.delete_project_loops, "Delete All Loops...");
},
.node => |node| {
Expand Down Expand Up @@ -212,6 +215,7 @@ fn actionForCommand(command: c_int) Action {
ids.remote_project_info => .remote_project_info,
ids.close_project => .close_project,
ids.remove_project => .remove_project,
ids.trash_project => .trash_project,
ids.delete_project_loops => .delete_project_loops,
ids.new_quick_chat => .new_quick_chat,
else => .none,
Expand Down Expand Up @@ -264,6 +268,7 @@ test "destructive context actions cannot bypass a cancelled confirmation" {
try std.testing.expect(!shouldApply(.delete_edge, false));
try std.testing.expect(!shouldApply(.delete_quick_chat, false));
try std.testing.expect(!shouldApply(.remove_project, false));
try std.testing.expect(!shouldApply(.trash_project, false));
try std.testing.expect(!shouldApply(.delete_project_loops, false));
try std.testing.expect(shouldApply(.rename_node, false));
}
Expand Down Expand Up @@ -295,5 +300,6 @@ test "project context commands expose ingress management and safe destructive ac
try std.testing.expectEqual(Action.open_project, actionForCommand(ids.open_project));
try std.testing.expectEqual(Action.project_settings, actionForCommand(ids.project_settings));
try std.testing.expectEqual(Action.remove_project, actionForCommand(ids.remove_project));
try std.testing.expectEqual(Action.trash_project, actionForCommand(ids.trash_project));
try std.testing.expectEqual(Action.delete_project_loops, actionForCommand(ids.delete_project_loops));
}
Loading
Loading