feat: add input/checkbox to confirm dialog and multi-selection actions

Introduce richer confirmation dialogs and wire them through the UI so actions
can collect an optional single-line input and a checkbox option.

- ConfirmDialog: support optional input and checkbox (with defaultChecked),
  expose onConfirm(result: {checked, value}). Focus/selects input when present
  and blocks confirm while required inputs/checkboxes are missing.
- App: add askConfirmation(...) returning {confirmed, value, checked} and keep
  requestConfirmation(...) as a convenience boolean wrapper. Update answer flow
  to pass the full result. Use the new prompt in stashStatusFiles to collect a
  stash message and "include untracked" option before saving.
- Status/Explorer/Worktree: add multi-selection support for stop-tracking and
  stash operations. onStopTracking now accepts an array of paths and a new
  "selection" kind; status context menu shows selection counts and uses a
  CopyCheck icon for selections. Added corresponding i18n messages.

This change keeps existing UX but enables collecting extra confirmation data
and acting on multi-file selections.
This commit is contained in:
2026-09-17 22:58:30 +02:00
parent 8c99e15dc9
commit acf8898b98
6 changed files with 152 additions and 41 deletions
+47 -14
View File
@@ -438,8 +438,9 @@
let externalToolsDetectionUnavailable = false;
let errorMessage = "";
let operation = "";
type ConfirmAnswer = { confirmed: boolean; value: string; checked: boolean };
let confirmDialogRequest: ConfirmRequest | null = null;
let confirmDialogResolve: ((confirmed: boolean) => void) | null = null;
let confirmDialogResolve: ((answer: ConfirmAnswer) => void) | null = null;
let compareFrom = "";
let compareTo = "";
let comparison: GitCommitComparison | null = null;
@@ -1289,15 +1290,20 @@
setLanguage(next);
}
/** Show the confirmation dialog and resolve once the user answers. */
function requestConfirmation(request: ConfirmRequest): Promise<boolean> {
confirmDialogResolve?.(false);
/** Show the dialog and resolve with the answer, including input and checkbox. */
function askConfirmation(request: ConfirmRequest): Promise<ConfirmAnswer> {
confirmDialogResolve?.({ confirmed: false, value: "", checked: false });
confirmDialogRequest = request;
return new Promise<boolean>((resolve) => {
return new Promise<ConfirmAnswer>((resolve) => {
confirmDialogResolve = resolve;
});
}
/** Yes/no only, for the many confirmations that need nothing else. */
async function requestConfirmation(request: ConfirmRequest): Promise<boolean> {
return (await askConfirmation(request)).confirmed;
}
/** Confirmation shown before deleting a local or remote branch. */
function branchDeleteConfirmRequest(branch: GitBranchInfo, force: boolean): ConfirmRequest {
const remoteName = branch.remote ? branch.name.split("/")[0] : "";
@@ -1356,11 +1362,11 @@
};
}
function answerConfirmation(confirmed: boolean) {
function answerConfirmation(confirmed: boolean, value = "", checked = false) {
const resolve = confirmDialogResolve;
confirmDialogRequest = null;
confirmDialogResolve = null;
resolve?.(confirmed);
resolve?.({ confirmed, value, checked });
}
function applyThemePreference(next: AppTheme) {
@@ -4797,8 +4803,34 @@
}
async function stashStatusFiles(files: GitFileStatus[], label: string) {
const suffix = files.length === 1 ? files[0].path : `${label} (${files.length} files)`;
await saveStash(`Gitty: ${suffix}`, true, files);
if (files.length === 0) return;
const suffix = files.length === 1
? files[0].path
: label ? `${label} (${files.length} files)` : `${files.length} files`;
const fallbackMessage = `Gitty: ${suffix}`;
const answer = await askConfirmation({
eyebrow: t("confirm.stashFiles.eyebrow"),
title: files.length === 1 ? t("confirm.stashFiles.titleOne") : t("confirm.stashFiles.title", { count: files.length }),
message: t("confirm.stashFiles.message"),
items: files.map((file) => file.path),
input: {
label: t("confirm.stashFiles.inputLabel"),
placeholder: fallbackMessage,
value: fallbackMessage,
optional: true,
},
checkbox: {
label: t("confirm.stashFiles.untracked"),
note: t("confirm.stashFiles.untrackedNote"),
defaultChecked: true,
},
confirmLabel: t("stashes.save"),
danger: false,
});
if (!answer.confirmed) return;
await saveStash(answer.value.trim() || fallbackMessage, answer.checked, files);
}
async function applyStashEntry(stash: GitStash) {
@@ -4884,12 +4916,13 @@
});
}
async function stopTrackingTarget(target: string, kind: "file" | "folder") {
if (!activeRepoPath || !target) return;
async function stopTrackingTarget(targets: string[], kind: "file" | "folder" | "selection") {
const paths = targets.filter(Boolean);
if (!activeRepoPath || paths.length === 0) return;
await runOperation(`Stopping tracking for ${kind}`, async () => {
applyStatus(await untrackPaths(activeRepoPath, [target]));
applyStatus(await untrackPaths(activeRepoPath, paths));
await refreshExplorerFiles(activeRepoPath);
trackEvent("git_paths_untracked", { kind });
trackEvent("git_paths_untracked", { kind, paths: paths.length });
});
}
@@ -6608,7 +6641,7 @@
{#if confirmDialogRequest}
<ConfirmDialog
request={confirmDialogRequest}
onConfirm={() => answerConfirmation(true)}
onConfirm={(result) => answerConfirmation(true, result.value, result.checked)}
onCancel={() => answerConfirmation(false)}
/>
{/if}