Command-History: stabile ID je Eintrag statt Index+Text-Abgleich
write_commands vergibt jedem Eintrag eine UUID; commands_delete löscht darüber. Doppelklick oder ein zweites Fenster laufen ins 'bereits entfernt' statt auf verschobene Indizes.
This commit is contained in:
+11
-1
@@ -276,7 +276,17 @@ fn call_write(req: &Value) -> Value {
|
|||||||
static SESSION_MARKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
static SESSION_MARKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
fn call_write_commands(req: &Value) -> Value {
|
fn call_write_commands(req: &Value) -> Value {
|
||||||
let commands = req["params"]["arguments"]["commands"].clone();
|
// Jeder Eintrag bekommt beim Schreiben eine stabile ID — das Panel löscht
|
||||||
|
// darüber, statt Positionen über Watcher-Latenz und Fenstergrenzen zu
|
||||||
|
// reichen (der frühere Index+Text-Abgleich war ein Provisorium).
|
||||||
|
let mut commands = req["params"]["arguments"]["commands"].clone();
|
||||||
|
if let Some(arr) = commands.as_array_mut() {
|
||||||
|
for c in arr {
|
||||||
|
if let Some(obj) = c.as_object_mut() {
|
||||||
|
obj.insert("id".into(), json!(uuid::Uuid::new_v4().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let count = commands.as_array().map(Vec::len).unwrap_or(0);
|
let count = commands.as_array().map(Vec::len).unwrap_or(0);
|
||||||
let path = match env_path("AI_CONTROL_COMMANDS") {
|
let path = match env_path("AI_CONTROL_COMMANDS") {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
|
|||||||
+15
-30
@@ -270,27 +270,12 @@ pub fn commands_read(project: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Entfernt einen Befehl aus der Command-History (Löschen einer Kachel im
|
/// Entfernt einen Befehl aus der Command-History (Löschen einer Kachel im
|
||||||
/// Panel). `line` ist der Index der nicht-leeren JSONL-Zeile, `entry` der
|
/// Panel) über seine stabile ID, die write_commands beim Schreiben vergibt.
|
||||||
/// Index im commands-Array des Records; ein leer gewordener Record fällt mit
|
/// Ein leer gewordener Record fällt mit weg; der Watcher meldet den neuen
|
||||||
/// weg. Der Watcher meldet den neuen Stand als `commands-update`.
|
/// Stand als `commands-update`. Doppelklick oder ein zweites Fenster auf
|
||||||
///
|
/// derselben Liste laufen ins „bereits entfernt" statt auf falsche Indizes.
|
||||||
/// `cmd` ist der angezeigte Befehlstext und wird vor dem Löschen abgeglichen:
|
|
||||||
/// Die Indizes des Panels sind der Datei um bis zu ein Watcher-Intervall
|
|
||||||
/// hinterher, und ein leer gewordener Record verschiebt alle folgenden. Ohne
|
|
||||||
/// Abgleich löscht ein Doppelklick — oder das zweite Fenster auf derselben
|
|
||||||
/// Liste — die falsche Kachel oder greift ins Leere.
|
|
||||||
///
|
|
||||||
/// Bewusst ein Provisorium, keine echte Identität: Zwei gleiche Befehle im
|
|
||||||
/// selben Record sind so nicht unterscheidbar. Sauber wäre eine stabile ID, die
|
|
||||||
/// beim Schreiben der JSONL vergeben wird, statt Positionen über die
|
|
||||||
/// Fenstergrenze zu reichen.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn commands_delete(
|
pub fn commands_delete(project: String, id: String) -> Result<(), String> {
|
||||||
project: String,
|
|
||||||
line: usize,
|
|
||||||
entry: usize,
|
|
||||||
cmd: String,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let path = commands_file(&project);
|
let path = commands_file(&project);
|
||||||
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||||
let mut records: Vec<serde_json::Value> = text
|
let mut records: Vec<serde_json::Value> = text
|
||||||
@@ -298,18 +283,18 @@ pub fn commands_delete(
|
|||||||
.filter(|l| !l.trim().is_empty())
|
.filter(|l| !l.trim().is_empty())
|
||||||
.map(|l| serde_json::from_str(l).map_err(|e| e.to_string()))
|
.map(|l| serde_json::from_str(l).map_err(|e| e.to_string()))
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
let cmds = records
|
let mut found = false;
|
||||||
.get_mut(line)
|
for rec in records.iter_mut() {
|
||||||
.ok_or("Befehl bereits entfernt")?["commands"]
|
if let Some(cmds) = rec["commands"].as_array_mut() {
|
||||||
.as_array_mut()
|
let before = cmds.len();
|
||||||
.ok_or("Record ohne commands")?;
|
cmds.retain(|c| c["id"].as_str() != Some(id.as_str()));
|
||||||
if cmds.get(entry).and_then(|c| c["cmd"].as_str()) != Some(cmd.as_str()) {
|
found = found || cmds.len() != before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
return Err("Befehl bereits entfernt".into());
|
return Err("Befehl bereits entfernt".into());
|
||||||
}
|
}
|
||||||
cmds.remove(entry);
|
records.retain(|r| r["commands"].as_array().is_none_or(|c| !c.is_empty()));
|
||||||
if cmds.is_empty() {
|
|
||||||
records.remove(line);
|
|
||||||
}
|
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for rec in &records {
|
for rec in &records {
|
||||||
out.push_str(&rec.to_string());
|
out.push_str(&rec.to_string());
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ describe("initCommandsView", () => {
|
|||||||
JSON.stringify({ ts: 1700000000, session: true }),
|
JSON.stringify({ ts: 1700000000, session: true }),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
ts: 1700000100,
|
ts: 1700000100,
|
||||||
commands: [{ cmd: "ls -la", note: "Liste" }, { cmd: "pwd" }],
|
commands: [{ cmd: "ls -la", note: "Liste", id: "id-1" }, { cmd: "pwd", id: "id-2" }],
|
||||||
}),
|
}),
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
@@ -94,14 +94,14 @@ describe("initCommandsView", () => {
|
|||||||
expect(view.empty()).toBe(true);
|
expect(view.empty()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Der Befehlstext geht mit, weil die Indizes der Datei hinterherhinken
|
/// Gelöscht wird über die stabile ID aus write_commands — keine Indizes,
|
||||||
/// können; das Backend gleicht damit ab, bevor es löscht.
|
/// die der Datei hinterherhinken könnten.
|
||||||
it("meldet Löschen mit Record-Index, Eintrags-Index und Befehl", () => {
|
it("meldet Löschen mit der Eintrags-ID", () => {
|
||||||
const onDelete = vi.fn();
|
const onDelete = vi.fn();
|
||||||
const view = initCommandsView(document.getElementById("c")!, onDelete);
|
const view = initCommandsView(document.getElementById("c")!, onDelete);
|
||||||
view.set(jsonl);
|
view.set(jsonl);
|
||||||
const dels = document.querySelectorAll<HTMLElement>(".cmd-del");
|
const dels = document.querySelectorAll<HTMLElement>(".cmd-del");
|
||||||
dels[1].click(); // zweite Kachel im Record 1
|
dels[1].click(); // zweite Kachel im Record 1
|
||||||
expect(onDelete).toHaveBeenCalledWith(1, 1, "pwd");
|
expect(onDelete).toHaveBeenCalledWith("id-2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// Kachel-Ansicht der Command-History: rendert die JSONL-Datei
|
/// Kachel-Ansicht der Command-History: rendert die JSONL-Datei
|
||||||
/// (write_commands im MCP-Server) als kopierbare Kacheln, Neuestes oben,
|
/// (write_commands im MCP-Server) als kopierbare Kacheln, Neuestes oben,
|
||||||
/// mit Zeitmarken und Session-Trennern. Löschen einer Kachel geht als
|
/// mit Zeitmarken und Session-Trennern. Löschen einer Kachel geht als
|
||||||
/// onDelete(line, entry) an den Aufrufer; der neue Stand kommt über den
|
/// onDelete(id) an den Aufrufer; der neue Stand kommt über den
|
||||||
/// Watcher zurück. DOM wird per createElement gebaut — Befehle sind
|
/// Watcher zurück. DOM wird per createElement gebaut — Befehle sind
|
||||||
/// Fremdtext und gehen nie durch innerHTML.
|
/// Fremdtext und gehen nie durch innerHTML.
|
||||||
|
|
||||||
@@ -17,6 +17,8 @@ function stripInvisibles(s: string): string {
|
|||||||
interface CommandEntry {
|
interface CommandEntry {
|
||||||
cmd: string;
|
cmd: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
/// Stabile ID aus write_commands; Grundlage fürs Löschen.
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Record {
|
interface Record {
|
||||||
@@ -79,7 +81,7 @@ function deleteBtn(onClick: () => void): HTMLButtonElement {
|
|||||||
|
|
||||||
export function initCommandsView(
|
export function initCommandsView(
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
onDelete: (line: number, entry: number, cmd: string) => void,
|
onDelete: (id: string) => void,
|
||||||
): CommandsView {
|
): CommandsView {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ export function initCommandsView(
|
|||||||
}
|
}
|
||||||
block.append(head);
|
block.append(head);
|
||||||
|
|
||||||
cmds.forEach((entry, j) => {
|
cmds.forEach((entry) => {
|
||||||
// Unsichtbare Steuerzeichen (Bidi, Zero-Width) aus Anzeige UND
|
// Unsichtbare Steuerzeichen (Bidi, Zero-Width) aus Anzeige UND
|
||||||
// Kopie halten — sonst sieht der Nutzer einen anderen Befehl, als
|
// Kopie halten — sonst sieht der Nutzer einen anderen Befehl, als
|
||||||
// die Zwischenablage enthält. Der Lösch-Abgleich läuft weiter über
|
// die Zwischenablage enthält. Der Lösch-Abgleich läuft weiter über
|
||||||
@@ -140,7 +142,7 @@ export function initCommandsView(
|
|||||||
note.textContent = entry.note;
|
note.textContent = entry.note;
|
||||||
body.append(note);
|
body.append(note);
|
||||||
}
|
}
|
||||||
tile.append(body, copyBtn(() => visible), deleteBtn(() => onDelete(i, j, entry.cmd)));
|
tile.append(body, copyBtn(() => visible), deleteBtn(() => onDelete(entry.id ?? "")));
|
||||||
block.append(tile);
|
block.append(tile);
|
||||||
});
|
});
|
||||||
container.append(block);
|
container.append(block);
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ export async function wirePanel(
|
|||||||
});
|
});
|
||||||
const cmdView = initCommandsView(
|
const cmdView = initCommandsView(
|
||||||
document.getElementById("commands-content")!,
|
document.getElementById("commands-content")!,
|
||||||
(line, entry, cmd) => invoke("commands_delete", { project, line, entry, cmd }),
|
(id) => invoke("commands_delete", { project, id }),
|
||||||
);
|
);
|
||||||
// Treffer-Klick lädt das Dokument in den Dokument-Tab (dort editier- und
|
// Treffer-Klick lädt das Dokument in den Dokument-Tab (dort editier- und
|
||||||
// archivierbar); der Sprung ins Wiki geht von dort aus.
|
// archivierbar); der Sprung ins Wiki geht von dort aus.
|
||||||
|
|||||||
Reference in New Issue
Block a user