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
56 changes: 40 additions & 16 deletions graphcode-windows/src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1252,12 +1252,26 @@ pub const App = struct {
self.setStatus("A clone is already running");
return;
}
self.clone_operation = RepositoryDialogs.CloneOperation.start(self.allocator, draft) catch {
const operation = RepositoryDialogs.CloneOperation.start(self.allocator, draft) catch {
self.setIngressError("Clone could not start");
self.setStatus("Clone could not start");
return;
};
self.setStatus("Cloning repository… (Ctrl+Shift+X cancels)");
defer operation.deinit();
const clone_status = RepositoryDialogs.showCloneProgress(self.window.hwnd, self.allocator, operation) catch {
operation.cancel();
self.setIngressError("Clone progress sheet could not open");
self.setStatus("Clone progress sheet could not open");
return;
};
switch (clone_status) {
.finished => self.setStatus("Repository cloned"),
.cancelled => self.setStatus("Clone cancelled"),
else => {
self.setIngressError("Clone failed");
self.setStatus("Clone failed");
},
}
}

fn cancelClone(self: *App) void {
Expand Down Expand Up @@ -1440,11 +1454,15 @@ pub const App = struct {
self.setStatus(@errorName(err));
return;
};
RepositoryDialogs.validateRemoteConnection(self.allocator, draft) catch |err| {
self.setIngressError(@errorName(err));
self.setStatus(@errorName(err));
if (!(RepositoryDialogs.showRemoteValidation(self.window.hwnd, self.allocator, draft) catch {
self.setIngressError("SSH validation could not start");
self.setStatus("SSH validation could not start");
return;
};
})) {
self.setIngressError("SSH connection validation failed");
self.setStatus("SSH connection validation failed");
return;
}
RepositoryDialogs.saveRemoteConfig(self.allocator, draft) catch {
self.setIngressError("SSH validated but remote configuration could not be saved");
self.setStatus("SSH validated but remote configuration could not be saved");
Expand Down Expand Up @@ -2116,7 +2134,7 @@ pub const App = struct {
var selected = std.array_list.Managed([]const u8).init(self.allocator);
defer selected.deinit();
for (current_inspection.entries.items[0..@min(current_inspection.entries.items.len, result.count)], 0..) |entry, index| {
if (result.selected[index] and WorktreeStatus.decision(entry) == .reclaimable)
if (result.selected[index] and WorktreeStatus.sweepSelectable(entry))
selected.append(entry.path) catch {
self.setStatus("Unable to collect Worktree Sweep selection");
return;
Expand All @@ -2133,13 +2151,14 @@ pub const App = struct {
};
var explicit_policy = WorktreeStatus.Policy{};
explicit_policy.applyResolveAction(.remove);
const removed = WorktreeStatus.reclaimSelectedWithPolicy(
const removed = WorktreeStatus.reclaimSelectedWithPolicyMode(
self.allocator,
project_path,
selected.items,
bindings.items,
explicit_policy,
true,
result.destructive_confirmed,
result.destructive_confirmed,
) catch |err| {
self.setStatus(switch (err) {
error.UnsafeSelection => "Worktree Sweep blocked an unsafe selection",
Expand Down Expand Up @@ -2513,18 +2532,15 @@ pub const App = struct {
dialog.policy
else
WorktreeStatus.loadPolicy(self.allocator, project_path);
const policy = NativeForms.worktreePolicy(self.window.hwnd, self.allocator, initial) catch {
const policy = NativeForms.worktreePolicy(self.window.hwnd, self.allocator, project_path, initial) catch {
self.setStatus("Unable to open worktree policy editor");
return;
} orelse {
self.setStatus("Worktree policy edit cancelled");
return;
};
self.saveWorktreePolicy(policy) catch {
self.setStatus("Unable to save project settings");
return;
};
self.setStatus("Project settings saved");
if (self.worktree_dialog) |*dialog| dialog.setPolicy(policy);
self.setStatus("Project settings updated");
}

fn saveCurrentWorktreePolicy(self: *App) void {
Expand Down Expand Up @@ -3909,7 +3925,15 @@ fn onWindowMessage(
c.WM_PAINT => {
var paint: c.PAINTSTRUCT = undefined;
const hdc = c.BeginPaint(hwnd, &paint);
const inspection = if (app.worktree_inspection) |*value| value else null;
const inspection = if (app.worktree_inspection) |*value| blk: {
const policy = WorktreeStatus.loadPolicy(app.allocator, value.project_path);
const summary = WorktreeStatus.summarize(value.entries.items);
var bytes: u64 = 0;
for (value.entries.items) |entry| bytes += entry.size_bytes;
const threshold_bytes = @as(u64, policy.notice_size_gb) * 1024 * 1024 * 1024;
if (summary.total >= policy.notice_count or bytes >= threshold_bytes) break :blk value;
break :blk null;
} else null;
app.update_lock.lock();
if (app.model.currentGraph()) |graph| app.canvas.syncNodeOffsets(graph.nodes.items);
const offered_version = if (app.update_state.state == .available) app.update_version else "";
Expand Down
133 changes: 108 additions & 25 deletions graphcode-windows/src/NativeForms.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ const DialogState = struct {
closed: bool = false,
scroll_offset: i32 = 0,
checks: [3]c.HWND = .{ null, null, null },
labels: [20]c.HWND = .{null} ** 20,
helps: [20]c.HWND = .{null} ** 20,
edits: [20]c.HWND = .{ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null },
input_kinds: [20]InputKind = .{.edit} ** 20,
choice_groups: [20]ChoiceGroup = .{.none} ** 20,
visible: [20]bool = .{false} ** 20,
labels: [256]c.HWND = .{null} ** 256,
helps: [256]c.HWND = .{null} ** 256,
edits: [256]c.HWND = .{null} ** 256,
input_kinds: [256]InputKind = .{.edit} ** 256,
choice_groups: [256]ChoiceGroup = .{.none} ** 256,
visible: [256]bool = .{false} ** 256,
field_count: usize = 0,
intro: c.HWND = null,
validation: c.HWND = null,
values: [20][]u8 = .{ &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{} },
initial_values: [20][]u8 = .{ &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{} },
display_labels: [20][]u8 = .{ &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{}, &.{} },
values: [256][]u8 = .{&.{}} ** 256,
initial_values: [256][]u8 = .{&.{}} ** 256,
display_labels: [256][]u8 = .{&.{}} ** 256,
sweep_selectable: [256]bool = .{false} ** 256,
sweep_paths: [256][]const u8 = .{&.{}} ** 256,
policy: WorktreeStatus.Policy = .{},
edge_endpoints: []const EdgeEndpoint = &.{},
lock_edge_endpoints: bool = true,
immediate_policy_path: []const u8 = "",
confirmation_armed: bool = false,
};

const Kind = enum { node, edge, update, settings, jump, worktree_policy, worktree_sweep };
Expand All @@ -34,12 +38,14 @@ const ChoiceGroup = enum { none, loop_type, backend, model_tier, metric_directio
const Choice = struct { label: []const u8, value: []const u8 };
pub const EdgeEndpoint = struct { id: []const u8, title: []const u8 };
pub const WorktreeSweepResult = struct {
selected: [20]bool = .{false} ** 20,
selected: [256]bool = .{false} ** 256,
count: usize = 0,
destructive_confirmed: bool = false,
};
const class_name = std.unicode.utf8ToUtf16LeStringLiteral("GraphCodeNativeForm");
const ok_id = 1;
const cancel_id = 2;
const reveal_id = 3;
var active_state: bool = false;
var active_state_storage: DialogState = undefined;

Expand Down Expand Up @@ -160,12 +166,12 @@ pub fn node(
state.values[19] = try allocator.dupe(u8, initial.created_by);
for (0..20) |index| state.initial_values[index] = try allocator.dupe(u8, state.values[index]);
if (!(try show(state, "Create or edit node", &.{}))) return null;
return try buildNodeDraft(allocator, state.values, initial);
return try buildNodeDraft(allocator, &state.values, initial);
}

fn buildNodeDraft(
allocator: std.mem.Allocator,
values: [20][]u8,
values: []const []u8,
initial: Forms.NodeDraft,
) !Forms.NodeDraft {
const goal_based = std.mem.eql(u8, values[1], "goalBased");
Expand Down Expand Up @@ -246,10 +252,10 @@ pub fn edgeWithEndpoints(
state.values[8] = try dupOptionalIntText(allocator, initial.cycle_stop_after_passes);
state.values[9] = try allocator.dupe(u8, initial.spawn_target_project_path);
if (!(try show(state, "Create or edit edge", &.{}))) return null;
return try buildEdgeDraft(allocator, state.values);
return try buildEdgeDraft(allocator, &state.values);
}

fn buildEdgeDraft(allocator: std.mem.Allocator, values: [20][]u8) !Forms.EdgeDraft {
fn buildEdgeDraft(allocator: std.mem.Allocator, values: []const []u8) !Forms.EdgeDraft {
const cycle_max = parseOptionalInt(values[7]) catch return error.InvalidNumericInput;
const cycle_stop = parseOptionalInt(values[8]) catch return error.InvalidNumericInput;
var result = Forms.EdgeDraft{ .from = &.{}, .to = &.{}, .kind = &.{}, .condition = &.{}, .transform_kind = &.{}, .transform_value = &.{}, .cycle_until = &.{}, .spawn_target_project_path = &.{} };
Expand Down Expand Up @@ -407,9 +413,15 @@ pub fn jump(parent: c.HWND, allocator: std.mem.Allocator, initial: []const u8) !
return try allocator.dupe(u8, state.values[0]);
}

pub fn worktreePolicy(parent: c.HWND, allocator: std.mem.Allocator, initial: WorktreeStatus.Policy) !?WorktreeStatus.Policy {
pub fn worktreePolicy(parent: c.HWND, allocator: std.mem.Allocator, project_path: []const u8, initial: WorktreeStatus.Policy) !?WorktreeStatus.Policy {
const state = try allocator.create(DialogState);
state.* = .{ .allocator = allocator, .kind = .worktree_policy, .parent = parent, .policy = initial };
state.* = .{
.allocator = allocator,
.kind = .worktree_policy,
.parent = parent,
.policy = initial,
.immediate_policy_path = project_path,
};
defer {
freeValues(state);
allocator.destroy(state);
Expand Down Expand Up @@ -442,21 +454,45 @@ pub fn worktreeSweep(
else
"LOOK BEFORE REMOVING";
const branch = if (entry.branch.len != 0) entry.branch else entry.path;
const size = WorktreeStatus.sizeText(allocator, entry.size_bytes) catch allocator.dupe(u8, "size unavailable") catch &.{};
state.display_labels[index] = try std.fmt.allocPrint(
allocator,
"{s}: {s} - {s}",
.{ tier, branch, WorktreeStatus.failureReasonText(entry) },
"{s}: {s} - {s} - {s}",
.{ tier, branch, WorktreeStatus.failureReasonText(entry), size },
);
state.values[index] = try allocator.dupe(u8, if (WorktreeStatus.decision(entry) == .reclaimable) "true" else "false");
allocator.free(size);
state.values[index] = try allocator.dupe(u8, if (WorktreeStatus.sweepSelectable(entry) and WorktreeStatus.decision(entry) == .reclaimable) "true" else "false");
state.initial_values[index] = try allocator.dupe(u8, state.values[index]);
state.input_kinds[index] = .checkbox;
state.visible[index] = true;
state.sweep_selectable[index] = WorktreeStatus.sweepSelectable(entry);
state.sweep_paths[index] = entry.path;
}
state.field_count = count;
const title = try std.fmt.allocPrint(allocator, "Worktrees - {s}", .{project_name});
var total_bytes: u64 = 0;
var safe_count: usize = 0;
var look_count: usize = 0;
var in_use_count: usize = 0;
for (entries[0..count]) |entry| {
total_bytes += entry.size_bytes;
if (entry.primary or entry.bound_running) {
in_use_count += 1;
} else if (WorktreeStatus.decision(entry) == .reclaimable) {
safe_count += 1;
} else {
look_count += 1;
}
}
const total_text = try WorktreeStatus.sizeText(allocator, total_bytes);
defer allocator.free(total_text);
const title = try std.fmt.allocPrint(
allocator,
"Worktrees - {s} ({d} total, {d} safe, {d} look, {d} in use, {s})",
.{ project_name, count, safe_count, look_count, in_use_count, total_text },
);
defer allocator.free(title);
if (!(try show(state, title, &.{}))) return null;
var result = WorktreeSweepResult{ .count = count };
var result = WorktreeSweepResult{ .count = count, .destructive_confirmed = state.confirmation_armed };
for (0..count) |index| result.selected[index] = std.mem.eql(u8, state.values[index], "true");
return result;
}
Expand Down Expand Up @@ -692,6 +728,7 @@ fn windowProc(hwnd: c.HWND, message: c.UINT, wparam: c.WPARAM, lparam: c.LPARAM)
var client: c.RECT = undefined;
_ = c.GetClientRect(safe_hwnd, &client);
createButton(safe_hwnd, if (value.kind == .node) "Create" else if (value.kind == .worktree_policy) "Done" else if (value.kind == .worktree_sweep) "Remove Selected" else "OK", ok_id, 478, client.bottom - 38);
if (value.kind == .worktree_sweep) createButton(safe_hwnd, "Show in Explorer", reveal_id, 300, client.bottom - 38);
createButton(safe_hwnd, "Cancel", cancel_id, 393, client.bottom - 38);
return 0;
},
Expand Down Expand Up @@ -749,16 +786,40 @@ fn windowProc(hwnd: c.HWND, message: c.UINT, wparam: c.WPARAM, lparam: c.LPARAM)
}
if ((notification == c.EN_CHANGE or notification == c.BN_CLICKED) and command >= 9100 and command < 9120)
setStaticText(value, value.validation, "");
if (value.kind == .worktree_policy and
(notification == c.EN_CHANGE or notification == c.BN_CLICKED) and
command >= 9100 and command < 9200)
{
readPolicy(value);
persistPolicy(value);
}
if (command == ok_id) {
readValues(value);
readPolicy(value);
if (value.kind == .worktree_sweep and hasDestructiveSelection(value) and !value.confirmation_armed) {
value.confirmation_armed = true;
setStaticText(value, value.validation, "Selected rows contain uncommitted files. Press Remove Selected again to remove anyway.");
return 0;
}
if (validationReason(value)) |reason| {
setStaticText(value, value.validation, reason);
} else {
applyModalCommand(value, .submit);
}
return 0;
}
if (command == reveal_id and value.kind == .worktree_sweep) {
for (0..value.field_count) |index| {
if (!std.mem.eql(u8, value.values[index], "true")) continue;
const parameters = WorktreeStatus.explorerParameters(value.allocator, value.sweep_paths[index]) catch break;
defer value.allocator.free(parameters);
const wide = utf8ToWideZ(value.allocator, parameters) catch break;
defer value.allocator.free(wide);
_ = c.ShellExecuteW(safe_hwnd, std.unicode.utf8ToUtf16LeStringLiteral("open").ptr, std.unicode.utf8ToUtf16LeStringLiteral("explorer.exe").ptr, wide.ptr, null, c.SW_SHOWNORMAL);
break;
}
return 0;
}
if (command == cancel_id) {
applyModalCommand(value, .cancel);
return 0;
Expand Down Expand Up @@ -893,7 +954,7 @@ fn createField(hwnd: c.HWND, state: *DialogState, index: usize) void {
},
.checkbox => {
_ = c.SendMessageW(input, c.BM_SETCHECK, if (std.mem.eql(u8, state.values[index], "true")) c.BST_CHECKED else c.BST_UNCHECKED, 0);
if (state.kind == .worktree_sweep and !std.mem.eql(u8, state.initial_values[index], "true"))
if (state.kind == .worktree_sweep and !state.sweep_selectable[index])
_ = c.EnableWindow(input, 0);
},
else => {
Expand Down Expand Up @@ -1142,6 +1203,28 @@ fn readPolicy(state: *DialogState) void {
state.policy.notice_count = std.fmt.parseInt(u32, std.mem.trim(u8, state.values[1], " \t\r\n"), 10) catch state.policy.notice_count;
}

fn persistPolicy(state: *DialogState) void {
if (state.immediate_policy_path.len == 0) return;
const size = std.fmt.parseInt(u32, std.mem.trim(u8, state.values[0], " \t\r\n"), 10) catch return;
const count = std.fmt.parseInt(u32, std.mem.trim(u8, state.values[1], " \t\r\n"), 10) catch return;
if (size == 0 or count == 0) return;
WorktreeStatus.savePolicy(state.allocator, state.immediate_policy_path, state.policy) catch {
setStaticText(state, state.validation, "Unable to save project settings");
};
}

fn hasDestructiveSelection(state: *const DialogState) bool {
if (state.kind != .worktree_sweep) return false;
for (0..state.field_count) |index| {
if (!state.sweep_selectable[index] or !std.mem.eql(u8, state.values[index], "true")) continue;
if (std.mem.indexOf(u8, state.display_labels[index], "local changes") != null or
std.mem.indexOf(u8, state.display_labels[index], "untracked files") != null or
std.mem.indexOf(u8, state.display_labels[index], "merge conflicts") != null)
return true;
}
return false;
}

fn validationReason(state: *DialogState) ?[]const u8 {
switch (state.kind) {
.node => {
Expand Down Expand Up @@ -1337,7 +1420,7 @@ test "node draft builder preserves every hidden initial field" {
.briefing_enabled = false,
.activity_enabled = true,
};
var draft = try buildNodeDraft(std.testing.allocator, values, initial);
var draft = try buildNodeDraft(std.testing.allocator, &values, initial);
defer draft.deinit(std.testing.allocator);
try std.testing.expectEqualStrings(initial.worktree_repository, draft.worktree_repository);
try std.testing.expectEqualStrings(initial.worktree_id, draft.worktree_id);
Expand All @@ -1351,7 +1434,7 @@ test "node draft builder preserves every hidden initial field" {
var hidden_values = values;
hidden_values[8] = @constCast("not-a-number");
hidden_values[9] = @constCast("also-invalid");
var hidden_draft = try buildNodeDraft(std.testing.allocator, hidden_values, initial);
var hidden_draft = try buildNodeDraft(std.testing.allocator, &hidden_values, initial);
defer hidden_draft.deinit(std.testing.allocator);
try std.testing.expectEqual(initial.poll_interval_seconds, hidden_draft.poll_interval_seconds);
try std.testing.expectEqual(initial.stall_after_seconds, hidden_draft.stall_after_seconds);
Expand Down Expand Up @@ -1391,7 +1474,7 @@ test "conditional graph fields and validation follow selected types" {
edge_state.values[7] = @constCast("4");
edge_state.values[8] = @constCast("2");
edge_state.values[9] = @constCast("D:\\other-project");
var edge_draft = try buildEdgeDraft(std.testing.allocator, edge_state.values);
var edge_draft = try buildEdgeDraft(std.testing.allocator, &edge_state.values);
defer edge_draft.deinit(std.testing.allocator);
try std.testing.expectEqualStrings("test -f done.flag", edge_draft.cycle_until);
try std.testing.expectEqual(@as(?i64, 4), edge_draft.cycle_max_iterations);
Expand Down
Loading
Loading