feat(hashline): implemented clipboard operations in hashline engine

- Implemented clipboard register management, parsing, and execution rules for CUT, COPY, and PASTE operations in the hashline engine.
- Added session-persistent clipboard state and integration across agent session execution, diff previews, and streaming tools.
- Added comprehensive validation, error messages, recovery handling, and test coverage for clipboard and block operations.
This commit is contained in:
can1357
2026-07-30 06:13:58 +02:00
parent 52bd191b34
commit 9aada058ee
29 changed files with 1389 additions and 120 deletions
+68 -1
View File
@@ -2,7 +2,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { formatHashlineHeader, InMemorySnapshotStore, missingSnapshotTagMessage } from "@oh-my-pi/hashline";
import {
forkClipboard,
formatHashlineHeader,
InMemorySnapshotStore,
missingSnapshotTagMessage,
} from "@oh-my-pi/hashline";
import {
adjustIndentation,
computeEditDiff,
@@ -268,6 +273,68 @@ describe("computeHashlineDiff", () => {
}
});
test("previews a same-file CUT + PASTE as the moved content", async () => {
const sourcePath = path.join(tempDir, "source.txt");
const text = "l1\nl2\nl3\n";
await Bun.write(sourcePath, text);
const snapshotStore = new InMemorySnapshotStore();
const tag = snapshotStore.record(sourcePath, text);
const result = await computeHashlineDiff(
{ input: `${formatHashlineHeader(sourcePath, tag)}\nCUT 1.=1\nPASTE.TAIL` },
tempDir,
snapshotStore,
);
expect("diff" in result).toBe(true);
if ("diff" in result) {
expect(result.diff).toContain("-1|l1");
expect(result.diff).toContain("+3|l1");
}
});
test("previews a PASTE from a forked session register without touching the source", async () => {
const sourcePath = path.join(tempDir, "source.txt");
const text = "l1\n";
await Bun.write(sourcePath, text);
const snapshotStore = new InMemorySnapshotStore();
const tag = snapshotStore.record(sourcePath, text);
// Mirror the streaming-strategy contract: the session register is forked
// once per preview frame; sections then thread the fork in patch order.
const sessionRegister = { lines: ["carried"], pendingCut: "CUT 5" };
const result = await computeHashlineDiff(
{ input: `${formatHashlineHeader(sourcePath, tag)}\nPASTE.TAIL` },
tempDir,
snapshotStore,
{ clipboard: forkClipboard(sessionRegister) },
);
expect("diff" in result).toBe(true);
if ("diff" in result) {
expect(result.diff).toContain("|carried");
}
expect(sessionRegister.pendingCut).toBe("CUT 5");
});
test("streams paste rows in the natural-order preview", async () => {
const sourcePath = path.join(tempDir, "source.txt");
const text = "l1\nl2\nl3\n";
await Bun.write(sourcePath, text);
const snapshotStore = new InMemorySnapshotStore();
const tag = snapshotStore.record(sourcePath, text);
const result = await computeHashlineDiff(
{ input: `${formatHashlineHeader(sourcePath, tag)}\nCUT 2.=2\nPASTE.HEAD` },
tempDir,
snapshotStore,
{ streaming: true, skipHashValidation: true },
);
expect("diff" in result).toBe(true);
if ("diff" in result) {
expect(result.diff).toContain("-2|l2");
expect(result.diff).toContain("+1|l2");
}
});
test("rejects a tagless head/tail insert in the preview path, matching apply", async () => {
const relativePath = "source.txt";
await Bun.write(path.join(tempDir, relativePath), "first\n");