auto-git:
[change] index.html [change] renderer.js [change] settings.html [change] src-tauri/src/main.rs [change] tauriBridge.js
This commit is contained in:
59
index.html
59
index.html
@@ -310,6 +310,65 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="rewordModeModal"
|
||||||
|
class="hidden fixed inset-0 z-50 items-center justify-center bg-black/40 px-4"
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="rewordModeTitle">
|
||||||
|
<div class="w-full max-w-md rounded-lg border bg-white p-5 shadow-xl"
|
||||||
|
style="border-color: var(--border)">
|
||||||
|
<h3 id="rewordModeTitle" class="text-lg font-semibold text-gray-900">
|
||||||
|
Reword commit message
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">
|
||||||
|
Choose Automatic to ask the configured LLM, or Manual to type the message yourself.
|
||||||
|
</p>
|
||||||
|
<label class="mt-4 flex items-center gap-2 text-sm text-gray-700 select-none">
|
||||||
|
<input id="rememberRewordMode" type="checkbox" class="h-4 w-4" />
|
||||||
|
Remember my choice
|
||||||
|
</label>
|
||||||
|
<div class="mt-5 flex justify-end gap-2">
|
||||||
|
<button id="cancelRewordModeBtn" class="px-3 py-2 text-sm border rounded hover:bg-gray-100">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="manualRewordModeBtn" class="px-3 py-2 text-sm border rounded hover:bg-gray-100">
|
||||||
|
Manual
|
||||||
|
</button>
|
||||||
|
<button id="autoRewordModeBtn" class="px-3 py-2 text-sm border rounded font-semibold"
|
||||||
|
style="background: var(--accent); color: #fff; border-color: var(--border)">
|
||||||
|
Automatic
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="manualRewordModal"
|
||||||
|
class="hidden fixed inset-0 z-50 items-center justify-center bg-black/40 px-4"
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="manualRewordTitle">
|
||||||
|
<form id="manualRewordForm"
|
||||||
|
class="w-full max-w-lg rounded-lg border bg-white p-5 shadow-xl"
|
||||||
|
style="border-color: var(--border)">
|
||||||
|
<h3 id="manualRewordTitle" class="text-lg font-semibold text-gray-900">
|
||||||
|
Enter commit message
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">
|
||||||
|
Type the new single-line message for this commit.
|
||||||
|
</p>
|
||||||
|
<input id="manualRewordInput" type="text" required autocomplete="off"
|
||||||
|
class="mt-4 w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:outline-none focus:ring-2"
|
||||||
|
style="--tw-ring-color: var(--accent)" />
|
||||||
|
<div class="mt-5 flex justify-end gap-2">
|
||||||
|
<button id="cancelManualRewordBtn" type="button"
|
||||||
|
class="px-3 py-2 text-sm border rounded hover:bg-gray-100">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="applyManualRewordBtn" type="submit"
|
||||||
|
class="px-3 py-2 text-sm border rounded font-semibold"
|
||||||
|
style="background: var(--accent); color: #fff; border-color: var(--border)">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="renderer.js"></script>
|
<script src="renderer.js"></script>
|
||||||
<script src="animeCat.js"></script>
|
<script src="animeCat.js"></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|||||||
115
renderer.js
115
renderer.js
@@ -10,6 +10,15 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
const initRepoBtn = document.getElementById('initRepoBtn');
|
const initRepoBtn = document.getElementById('initRepoBtn');
|
||||||
const pushBtn = document.getElementById('pushBtn');
|
const pushBtn = document.getElementById('pushBtn');
|
||||||
const rewritePendingBtn = document.getElementById('rewritePendingBtn');
|
const rewritePendingBtn = document.getElementById('rewritePendingBtn');
|
||||||
|
const rewordModeModal = document.getElementById('rewordModeModal');
|
||||||
|
const rememberRewordMode = document.getElementById('rememberRewordMode');
|
||||||
|
const cancelRewordModeBtn = document.getElementById('cancelRewordModeBtn');
|
||||||
|
const manualRewordModeBtn = document.getElementById('manualRewordModeBtn');
|
||||||
|
const autoRewordModeBtn = document.getElementById('autoRewordModeBtn');
|
||||||
|
const manualRewordModal = document.getElementById('manualRewordModal');
|
||||||
|
const manualRewordForm = document.getElementById('manualRewordForm');
|
||||||
|
const manualRewordInput = document.getElementById('manualRewordInput');
|
||||||
|
const cancelManualRewordBtn = document.getElementById('cancelManualRewordBtn');
|
||||||
const panel = document.querySelector('.flex-1.p-4.overflow-y-auto');
|
const panel = document.querySelector('.flex-1.p-4.overflow-y-auto');
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@@ -21,6 +30,59 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
let lastFolderPath = null;
|
let lastFolderPath = null;
|
||||||
let lastPage = null;
|
let lastPage = null;
|
||||||
|
|
||||||
|
function chooseRewordMode() {
|
||||||
|
rememberRewordMode.checked = false;
|
||||||
|
rewordModeModal.classList.remove('hidden');
|
||||||
|
rewordModeModal.classList.add('flex');
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const finish = mode => {
|
||||||
|
rewordModeModal.classList.add('hidden');
|
||||||
|
rewordModeModal.classList.remove('flex');
|
||||||
|
resolve(mode ? { mode, remember: rememberRewordMode.checked } : null);
|
||||||
|
};
|
||||||
|
cancelRewordModeBtn.onclick = () => finish(null);
|
||||||
|
manualRewordModeBtn.onclick = () => finish('manual');
|
||||||
|
autoRewordModeBtn.onclick = () => finish('auto');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptForCommitMessage(currentMessage) {
|
||||||
|
manualRewordInput.value = currentMessage || '';
|
||||||
|
manualRewordInput.setCustomValidity('');
|
||||||
|
manualRewordModal.classList.remove('hidden');
|
||||||
|
manualRewordModal.classList.add('flex');
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
manualRewordInput.focus();
|
||||||
|
manualRewordInput.select();
|
||||||
|
});
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const finish = message => {
|
||||||
|
manualRewordModal.classList.add('hidden');
|
||||||
|
manualRewordModal.classList.remove('flex');
|
||||||
|
resolve(message);
|
||||||
|
};
|
||||||
|
cancelManualRewordBtn.onclick = () => finish(null);
|
||||||
|
manualRewordInput.oninput = () => manualRewordInput.setCustomValidity('');
|
||||||
|
manualRewordInput.onkeydown = event => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
finish(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
manualRewordForm.onsubmit = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const message = manualRewordInput.value.trim();
|
||||||
|
if (!message) {
|
||||||
|
manualRewordInput.setCustomValidity('The commit message cannot be empty.');
|
||||||
|
manualRewordInput.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manualRewordInput.setCustomValidity('');
|
||||||
|
finish(message);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const slot = document.getElementById('catSlot');
|
const slot = document.getElementById('catSlot');
|
||||||
window.cat = new window.AnimeCat(slot, {
|
window.cat = new window.AnimeCat(slot, {
|
||||||
images: {
|
images: {
|
||||||
@@ -771,10 +833,61 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startTypedRewrite(hash, currentMessage, control) {
|
||||||
|
const message = await promptForCommitMessage(currentMessage);
|
||||||
|
if (message === null) return;
|
||||||
|
control.disabled = true;
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.rewriteCommitWithMessage(
|
||||||
|
folderObj.path,
|
||||||
|
hash,
|
||||||
|
message
|
||||||
|
);
|
||||||
|
if (!result?.success) {
|
||||||
|
alert('Rewrite could not start:\n' + (result?.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Typed commit rewrite error:', e);
|
||||||
|
alert('Rewrite could not start:\n' + (e.message || e));
|
||||||
|
} finally {
|
||||||
|
const refreshed = await getFolderObjByPath(folderObj.path);
|
||||||
|
if (refreshed) await renderContent(refreshed, currentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRewordButton(commit, control) {
|
||||||
|
let mode = 'ask';
|
||||||
|
try {
|
||||||
|
mode = await window.settingsAPI.getRewordMode();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not load Reword choice:', e);
|
||||||
|
}
|
||||||
|
if (!['ask', 'auto', 'manual'].includes(mode)) mode = 'ask';
|
||||||
|
if (mode === 'ask') {
|
||||||
|
const choice = await chooseRewordMode();
|
||||||
|
if (!choice) return;
|
||||||
|
mode = choice.mode;
|
||||||
|
if (choice.remember) {
|
||||||
|
try {
|
||||||
|
await window.settingsAPI.setRewordMode(mode);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not remember Reword choice:', e);
|
||||||
|
alert('Your choice could not be saved, but this rewrite will continue.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mode === 'manual') {
|
||||||
|
await startTypedRewrite(commit.hash, commit.message, control);
|
||||||
|
} else {
|
||||||
|
await startSingleRewrite(commit.hash, control);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reword is available for every commit in the current HEAD history.
|
// Reword is available for every commit in the current HEAD history.
|
||||||
contentList.querySelectorAll('.reword-btn').forEach(btn => {
|
contentList.querySelectorAll('.reword-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
await startSingleRewrite(btn.dataset.hash, btn);
|
const commit = commits.find(item => item.hash === btn.dataset.hash);
|
||||||
|
if (commit) await handleRewordButton(commit, btn);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@
|
|||||||
const cbSkip = document.getElementById('skipPrompt');
|
const cbSkip = document.getElementById('skipPrompt');
|
||||||
const thresholdInput = document.getElementById('intelligentCommitThresholdInput');
|
const thresholdInput = document.getElementById('intelligentCommitThresholdInput');
|
||||||
const minutesInput = document.getElementById('intelligentCommitMinutesInput');
|
const minutesInput = document.getElementById('intelligentCommitMinutesInput');
|
||||||
|
const rewordModeSelect = document.getElementById('rewordModeSelect');
|
||||||
const ok = document.getElementById('okBtn');
|
const ok = document.getElementById('okBtn');
|
||||||
const cancel = document.getElementById('cancelBtn');
|
const cancel = document.getElementById('cancelBtn');
|
||||||
const cbAutostart = document.getElementById('autostart');
|
const cbAutostart = document.getElementById('autostart');
|
||||||
@@ -190,11 +191,12 @@
|
|||||||
cbCloseToTray.checked = initialCloseToTray;
|
cbCloseToTray.checked = initialCloseToTray;
|
||||||
|
|
||||||
// Initialwerte parallel laden
|
// Initialwerte parallel laden
|
||||||
const [initialTheme, initialSkip, initialThreshold, initialMinutes] = await Promise.all([
|
const [initialTheme, initialSkip, initialThreshold, initialMinutes, initialRewordMode] = await Promise.all([
|
||||||
window.settingsAPI.getTheme(),
|
window.settingsAPI.getTheme(),
|
||||||
window.settingsAPI.getSkipPrompt(),
|
window.settingsAPI.getSkipPrompt(),
|
||||||
window.electronAPI.getIntelligentCommitThreshold(),
|
window.electronAPI.getIntelligentCommitThreshold(),
|
||||||
window.electronAPI.getMinutesCommitThreshold()
|
window.electronAPI.getMinutesCommitThreshold(),
|
||||||
|
window.settingsAPI.getRewordMode()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fallbackTheme = VALID_THEMES.includes(initialTheme) ? initialTheme : 'sky';
|
const fallbackTheme = VALID_THEMES.includes(initialTheme) ? initialTheme : 'sky';
|
||||||
@@ -204,6 +206,9 @@
|
|||||||
cbSkip.checked = initialSkip;
|
cbSkip.checked = initialSkip;
|
||||||
thresholdInput.value = initialThreshold;
|
thresholdInput.value = initialThreshold;
|
||||||
minutesInput.value = initialMinutes;
|
minutesInput.value = initialMinutes;
|
||||||
|
rewordModeSelect.value = ['ask', 'auto', 'manual'].includes(initialRewordMode)
|
||||||
|
? initialRewordMode
|
||||||
|
: 'ask';
|
||||||
|
|
||||||
// **Vorschau**: Theme sofort übernehmen
|
// **Vorschau**: Theme sofort übernehmen
|
||||||
themeSelect.addEventListener('change', async () => {
|
themeSelect.addEventListener('change', async () => {
|
||||||
@@ -224,6 +229,7 @@
|
|||||||
await window.settingsAPI.setAutostart(cbAutostart.checked);
|
await window.settingsAPI.setAutostart(cbAutostart.checked);
|
||||||
await window.settingsAPI.setCloseToTray(cbCloseToTray.checked);
|
await window.settingsAPI.setCloseToTray(cbCloseToTray.checked);
|
||||||
await window.settingsAPI.setTheme(themeSelect.value || fallbackTheme);
|
await window.settingsAPI.setTheme(themeSelect.value || fallbackTheme);
|
||||||
|
await window.settingsAPI.setRewordMode(rewordModeSelect.value || 'ask');
|
||||||
|
|
||||||
// Threshold speichern (immer Wert aus Feld nehmen!)
|
// Threshold speichern (immer Wert aus Feld nehmen!)
|
||||||
let threshold = parseInt(thresholdInput.value, 10);
|
let threshold = parseInt(thresholdInput.value, 10);
|
||||||
@@ -343,6 +349,14 @@
|
|||||||
<option value="grey">Grey</option>
|
<option value="grey">Grey</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="option" style="align-items:center;">
|
||||||
|
Reword button:
|
||||||
|
<select id="rewordModeSelect" style="margin-left:1em;">
|
||||||
|
<option value="ask">Ask every time</option>
|
||||||
|
<option value="auto">Automatic (LLM)</option>
|
||||||
|
<option value="manual">Manual input</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="option">
|
<label class="option">
|
||||||
<input type="checkbox" id="skipPrompt" class="flex-none" />
|
<input type="checkbox" id="skipPrompt" class="flex-none" />
|
||||||
Skip prompt asking to remove .git folder if it has only one commit and no changes
|
Skip prompt asking to remove .git folder if it has only one commit and no changes
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use tempfile::TempDir;
|
|||||||
type CommandResult<T> = Result<T, String>;
|
type CommandResult<T> = Result<T, String>;
|
||||||
|
|
||||||
const VALID_THEMES: &[&str] = &["sky", "default", "grey"];
|
const VALID_THEMES: &[&str] = &["sky", "default", "grey"];
|
||||||
|
const VALID_REWORD_MODES: &[&str] = &["ask", "auto", "manual"];
|
||||||
const EMPTY_TREE_HASH: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
const EMPTY_TREE_HASH: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||||
const SQUASH_CHUNK_WINDOW_MS: i64 = 2 * 60 * 1000;
|
const SQUASH_CHUNK_WINDOW_MS: i64 = 2 * 60 * 1000;
|
||||||
const MAX_SQUASH_PROMPT_CHARS: usize = 25_000;
|
const MAX_SQUASH_PROMPT_CHARS: usize = 25_000;
|
||||||
@@ -279,6 +280,8 @@ struct StoreData {
|
|||||||
commit_model: Option<String>,
|
commit_model: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
readme_model: Option<String>,
|
readme_model: Option<String>,
|
||||||
|
#[serde(default = "default_reword_mode")]
|
||||||
|
reword_mode: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
author: Option<String>,
|
author: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -305,6 +308,7 @@ impl Default for StoreData {
|
|||||||
llm_buffer: Vec::new(),
|
llm_buffer: Vec::new(),
|
||||||
commit_model: None,
|
commit_model: None,
|
||||||
readme_model: None,
|
readme_model: None,
|
||||||
|
reword_mode: default_reword_mode(),
|
||||||
author: None,
|
author: None,
|
||||||
license: None,
|
license: None,
|
||||||
}
|
}
|
||||||
@@ -315,6 +319,10 @@ fn default_theme() -> String {
|
|||||||
"sky".to_string()
|
"sky".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_reword_mode() -> String {
|
||||||
|
"ask".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -340,6 +348,7 @@ struct CommitPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
struct CommitSummary {
|
struct CommitSummary {
|
||||||
hash: String,
|
hash: String,
|
||||||
date: String,
|
date: String,
|
||||||
@@ -456,6 +465,9 @@ fn normalize_store(store: &mut StoreData) {
|
|||||||
store.theme = if store.skymode { "sky" } else { "default" }.to_string();
|
store.theme = if store.skymode { "sky" } else { "default" }.to_string();
|
||||||
}
|
}
|
||||||
store.skymode = store.theme == "sky";
|
store.skymode = store.theme == "sky";
|
||||||
|
if !VALID_REWORD_MODES.contains(&store.reword_mode.as_str()) {
|
||||||
|
store.reword_mode = default_reword_mode();
|
||||||
|
}
|
||||||
|
|
||||||
for folder in &mut store.folders {
|
for folder in &mut store.folders {
|
||||||
let repo_exists = Path::new(&folder.path).join(".git").exists();
|
let repo_exists = Path::new(&folder.path).join(".git").exists();
|
||||||
@@ -2051,7 +2063,7 @@ fn open_settings_window(app: &AppHandle) -> CommandResult<()> {
|
|||||||
let builder =
|
let builder =
|
||||||
WebviewWindowBuilder::new(app, "settings", WebviewUrl::App("settings.html".into()))
|
WebviewWindowBuilder::new(app, "settings", WebviewUrl::App("settings.html".into()))
|
||||||
.title("Einstellungen")
|
.title("Einstellungen")
|
||||||
.inner_size(600.0, 500.0)
|
.inner_size(600.0, 550.0)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.background_color(ROSE_TITLEBAR_COLOR);
|
.background_color(ROSE_TITLEBAR_COLOR);
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -2678,6 +2690,25 @@ fn set_readme_model(state: tauri::State<'_, AppState>, val: String) -> CommandRe
|
|||||||
save_store(&state)
|
save_store(&state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_reword_mode(state: tauri::State<'_, AppState>) -> CommandResult<String> {
|
||||||
|
Ok(state
|
||||||
|
.store
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.reword_mode
|
||||||
|
.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_reword_mode(state: tauri::State<'_, AppState>, val: String) -> CommandResult<()> {
|
||||||
|
if !VALID_REWORD_MODES.contains(&val.as_str()) {
|
||||||
|
return Err("Invalid Reword button behavior.".to_string());
|
||||||
|
}
|
||||||
|
state.store.lock().map_err(|e| e.to_string())?.reword_mode = val;
|
||||||
|
save_store(&state)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_intelligent_commit_threshold(state: tauri::State<'_, AppState>) -> CommandResult<i64> {
|
fn get_intelligent_commit_threshold(state: tauri::State<'_, AppState>) -> CommandResult<i64> {
|
||||||
Ok(state
|
Ok(state
|
||||||
@@ -2892,6 +2923,7 @@ fn run_manual_rewrite_job(
|
|||||||
folder_path: String,
|
folder_path: String,
|
||||||
hashes: Vec<String>,
|
hashes: Vec<String>,
|
||||||
scope: &'static str,
|
scope: &'static str,
|
||||||
|
supplied_messages: Option<HashMap<String, String>>,
|
||||||
) {
|
) {
|
||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
let total = hashes.len();
|
let total = hashes.len();
|
||||||
@@ -2948,40 +2980,58 @@ fn run_manual_rewrite_job(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut successful_messages = HashMap::new();
|
let mut successful_messages = supplied_messages.unwrap_or_default();
|
||||||
let mut llm_failures: Vec<(String, String)> = Vec::new();
|
let mut llm_failures: Vec<(String, String)> = Vec::new();
|
||||||
match ensure_ollama_running() {
|
let uses_llm = successful_messages.is_empty();
|
||||||
|
match if uses_llm {
|
||||||
|
ensure_ollama_running()
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
} {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
for (index, hash) in hashes.iter().enumerate() {
|
if !uses_llm {
|
||||||
emit_rewrite_progress(
|
emit_rewrite_progress(
|
||||||
&app,
|
&app,
|
||||||
&folder_path,
|
&folder_path,
|
||||||
scope,
|
scope,
|
||||||
"running",
|
"running",
|
||||||
index,
|
|
||||||
total,
|
total,
|
||||||
Some(hash),
|
total,
|
||||||
|
hashes.first().map(String::as_str),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
match generate_llm_message_for_commit(&app, &folder_path, hash, &model) {
|
} else {
|
||||||
Ok(message) => {
|
for (index, hash) in hashes.iter().enumerate() {
|
||||||
successful_messages.insert(hash.clone(), message);
|
emit_rewrite_progress(
|
||||||
}
|
&app,
|
||||||
Err(err) => {
|
&folder_path,
|
||||||
eprintln!("[manualRewrite] {} failed: {err}", short_hash(hash));
|
scope,
|
||||||
llm_failures.push((hash.clone(), err));
|
"running",
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
Some(hash),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
match generate_llm_message_for_commit(&app, &folder_path, hash, &model) {
|
||||||
|
Ok(message) => {
|
||||||
|
successful_messages.insert(hash.clone(), message);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("[manualRewrite] {} failed: {err}", short_hash(hash));
|
||||||
|
llm_failures.push((hash.clone(), err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
emit_rewrite_progress(
|
||||||
|
&app,
|
||||||
|
&folder_path,
|
||||||
|
scope,
|
||||||
|
"running",
|
||||||
|
index + 1,
|
||||||
|
total,
|
||||||
|
Some(hash),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
emit_rewrite_progress(
|
|
||||||
&app,
|
|
||||||
&folder_path,
|
|
||||||
scope,
|
|
||||||
"running",
|
|
||||||
index + 1,
|
|
||||||
total,
|
|
||||||
Some(hash),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -3142,7 +3192,67 @@ fn rewrite_commit(
|
|||||||
folder.rewrite_started_at = Some(now_ms());
|
folder.rewrite_started_at = Some(now_ms());
|
||||||
}
|
}
|
||||||
save_store(&state)?;
|
save_store(&state)?;
|
||||||
thread::spawn(move || run_manual_rewrite_job(app, folder_path, vec![full_hash], "single"));
|
thread::spawn(move || {
|
||||||
|
run_manual_rewrite_job(app, folder_path, vec![full_hash], "single", None)
|
||||||
|
});
|
||||||
|
Ok(json!({ "success": true, "count": 1 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn rewrite_commit_with_message(
|
||||||
|
app: AppHandle,
|
||||||
|
state: tauri::State<'_, AppState>,
|
||||||
|
folder_path: String,
|
||||||
|
hash: String,
|
||||||
|
message: String,
|
||||||
|
) -> CommandResult<Value> {
|
||||||
|
let message = message.trim().to_string();
|
||||||
|
if message.is_empty() {
|
||||||
|
return Ok(json!({ "success": false, "error": "The commit message cannot be empty." }));
|
||||||
|
}
|
||||||
|
if message.lines().count() != 1 {
|
||||||
|
return Ok(
|
||||||
|
json!({ "success": false, "error": "Please enter a single-line commit message." }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let full_hash = resolve_commit_hash(&folder_path, &hash)
|
||||||
|
.ok_or_else(|| format!("Commit {hash} was not found."))?;
|
||||||
|
if !is_commit_on_current_history(&folder_path, &full_hash) {
|
||||||
|
return Ok(json!({
|
||||||
|
"success": false,
|
||||||
|
"error": "This commit is not in the current HEAD history. Jump to its branch/history first."
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if is_rebase_in_progress(&folder_path) {
|
||||||
|
return Ok(json!({ "success": false, "error": "A Git rebase is already in progress." }));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut store = state.store.lock().map_err(|e| e.to_string())?;
|
||||||
|
let Some(folder) = store
|
||||||
|
.folders
|
||||||
|
.iter_mut()
|
||||||
|
.find(|folder| folder.path == folder_path)
|
||||||
|
else {
|
||||||
|
return Ok(json!({ "success": false, "error": "folder not found" }));
|
||||||
|
};
|
||||||
|
if folder.needs_relocation {
|
||||||
|
return Ok(json!({ "success": false, "error": "needs relocation" }));
|
||||||
|
}
|
||||||
|
if folder.rewrite_in_progress {
|
||||||
|
return Ok(json!({
|
||||||
|
"success": false,
|
||||||
|
"error": "Another rewrite is already running for this repository."
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
folder.rewrite_in_progress = true;
|
||||||
|
folder.rewrite_started_at = Some(now_ms());
|
||||||
|
}
|
||||||
|
save_store(&state)?;
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut messages = HashMap::new();
|
||||||
|
messages.insert(full_hash.clone(), message);
|
||||||
|
run_manual_rewrite_job(app, folder_path, vec![full_hash], "typed", Some(messages));
|
||||||
|
});
|
||||||
Ok(json!({ "success": true, "count": 1 }))
|
Ok(json!({ "success": true, "count": 1 }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3185,7 +3295,7 @@ fn rewrite_pending_commits(
|
|||||||
};
|
};
|
||||||
let count = hashes.len();
|
let count = hashes.len();
|
||||||
save_store(&state)?;
|
save_store(&state)?;
|
||||||
thread::spawn(move || run_manual_rewrite_job(app, folder_path, hashes, "pending"));
|
thread::spawn(move || run_manual_rewrite_job(app, folder_path, hashes, "pending", None));
|
||||||
Ok(json!({ "success": true, "count": count }))
|
Ok(json!({ "success": true, "count": count }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4164,6 +4274,8 @@ fn main() {
|
|||||||
set_commit_model,
|
set_commit_model,
|
||||||
get_readme_model,
|
get_readme_model,
|
||||||
set_readme_model,
|
set_readme_model,
|
||||||
|
get_reword_mode,
|
||||||
|
set_reword_mode,
|
||||||
get_intelligent_commit_threshold,
|
get_intelligent_commit_threshold,
|
||||||
set_intelligent_commit_threshold,
|
set_intelligent_commit_threshold,
|
||||||
get_minutes_commit_threshold,
|
get_minutes_commit_threshold,
|
||||||
@@ -4182,6 +4294,7 @@ fn main() {
|
|||||||
get_all_commit_hashes,
|
get_all_commit_hashes,
|
||||||
trigger_rewrite_now,
|
trigger_rewrite_now,
|
||||||
rewrite_commit,
|
rewrite_commit,
|
||||||
|
rewrite_commit_with_message,
|
||||||
rewrite_pending_commits,
|
rewrite_pending_commits,
|
||||||
show_folder_context_menu,
|
show_folder_context_menu,
|
||||||
show_tree_context_menu,
|
show_tree_context_menu,
|
||||||
@@ -4229,6 +4342,33 @@ mod tests {
|
|||||||
assert_eq!(slugify_repository_name("☃"), "repository");
|
assert_eq!(slugify_repository_name("☃"), "repository");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reword_button_mode_defaults_to_ask_and_invalid_values_are_normalized() {
|
||||||
|
let mut store = StoreData::default();
|
||||||
|
assert_eq!(store.reword_mode, "ask");
|
||||||
|
|
||||||
|
store.reword_mode = "unexpected".to_string();
|
||||||
|
normalize_store(&mut store);
|
||||||
|
assert_eq!(store.reword_mode, "ask");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_summary_uses_the_javascript_field_names() {
|
||||||
|
let summary = CommitSummary {
|
||||||
|
hash: "abcdef0".to_string(),
|
||||||
|
date: "2026-07-12T00:00:00Z".to_string(),
|
||||||
|
message: "Test message".to_string(),
|
||||||
|
needs_rewrite: true,
|
||||||
|
can_reword: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized = serde_json::to_value(summary).unwrap();
|
||||||
|
assert_eq!(serialized["needsRewrite"], true);
|
||||||
|
assert_eq!(serialized["canReword"], true);
|
||||||
|
assert!(serialized.get("needs_rewrite").is_none());
|
||||||
|
assert!(serialized.get("can_reword").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn existing_project_ignore_rules_are_written_before_staging() {
|
fn existing_project_ignore_rules_are_written_before_staging() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
setCommitModel: model => invoke('set_commit_model', { val: model }),
|
setCommitModel: model => invoke('set_commit_model', { val: model }),
|
||||||
getReadmeModel: () => invoke('get_readme_model'),
|
getReadmeModel: () => invoke('get_readme_model'),
|
||||||
setReadmeModel: model => invoke('set_readme_model', { val: model }),
|
setReadmeModel: model => invoke('set_readme_model', { val: model }),
|
||||||
|
getRewordMode: () => invoke('get_reword_mode'),
|
||||||
|
setRewordMode: val => invoke('set_reword_mode', { val }),
|
||||||
close: () => invoke('close_settings'),
|
close: () => invoke('close_settings'),
|
||||||
getAutostart: () => invoke('get_autostart'),
|
getAutostart: () => invoke('get_autostart'),
|
||||||
setAutostart: val => invoke('set_autostart', { enabled: val }),
|
setAutostart: val => invoke('set_autostart', { enabled: val }),
|
||||||
@@ -123,6 +125,7 @@
|
|||||||
initRepo: folderPath => invoke('init_repo', { folderPath }),
|
initRepo: folderPath => invoke('init_repo', { folderPath }),
|
||||||
triggerRewriteNow: folderPath => invoke('trigger_rewrite_now', { folderPath }),
|
triggerRewriteNow: folderPath => invoke('trigger_rewrite_now', { folderPath }),
|
||||||
rewriteCommit: (folderPath, hash) => invoke('rewrite_commit', { folderPath, hash }),
|
rewriteCommit: (folderPath, hash) => invoke('rewrite_commit', { folderPath, hash }),
|
||||||
|
rewriteCommitWithMessage: (folderPath, hash, message) => invoke('rewrite_commit_with_message', { folderPath, hash, message }),
|
||||||
rewritePendingCommits: folderPath => invoke('rewrite_pending_commits', { folderPath })
|
rewritePendingCommits: folderPath => invoke('rewrite_pending_commits', { folderPath })
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user