pi-hashline-edit-pro 4.2.7 → 4.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/anchor-registry.ts +84 -19
- package/src/fs-write.ts +4 -2
- package/src/hash-store/retry.ts +1 -1
- package/src/hash-store.ts +10 -4
- package/src/replace-undo.ts +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
package/src/anchor-registry.ts
CHANGED
|
@@ -152,13 +152,14 @@ interface SessionState {
|
|
|
152
152
|
served: Map<string, Map<string, string>>;
|
|
153
153
|
everMinted: AnchorMintedSet;
|
|
154
154
|
probe: number;
|
|
155
|
+
allocatedChecksum: Map<string, string>;
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
const SIDECAR_SUFFIX = ".registry.jsonl";
|
|
158
159
|
const SIDECAR_COMPACT_LINES = 5000;
|
|
159
160
|
const SIDECAR_COMPACT_BYTES = 1024 * 1024;
|
|
160
|
-
const
|
|
161
|
-
const SIDECAR_HEADER_BYTES = 64 * 1024;
|
|
161
|
+
export const SIDECAR_COMPACT_LINE_BYTES = 48 * 1024;
|
|
162
|
+
export const SIDECAR_HEADER_BYTES = 64 * 1024;
|
|
162
163
|
const SIDECAR_HEADER_CHUNK = 4096;
|
|
163
164
|
let currentKey: string | undefined;
|
|
164
165
|
const sidecarByKey = new Map<string, string>();
|
|
@@ -174,7 +175,7 @@ function newSessionState(seed?: string): SessionState {
|
|
|
174
175
|
probe = (probe * 256 + byte) % ANCHOR_COUNT;
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
|
-
return { owned: new Map(), served: new Map(), everMinted: new Set(), probe };
|
|
178
|
+
return { owned: new Map(), served: new Map(), everMinted: new Set(), probe, allocatedChecksum: new Map() };
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
function seedServedFromOwned(state: SessionState): void {
|
|
@@ -246,6 +247,45 @@ export function shouldCompactSidecar(raw: string): boolean {
|
|
|
246
247
|
for (let i = 0; i < raw.length; i++) if (raw.charCodeAt(i) === 10) lines += 1;
|
|
247
248
|
return lines >= SIDECAR_COMPACT_LINES;
|
|
248
249
|
}
|
|
250
|
+
|
|
251
|
+
function parseSessionFileLine(line: string): string | undefined {
|
|
252
|
+
const trimmed = line.trim();
|
|
253
|
+
if (trimmed.length === 0) return undefined;
|
|
254
|
+
try {
|
|
255
|
+
const event = JSON.parse(trimmed) as { kind?: unknown; sessionFile?: unknown };
|
|
256
|
+
if (event.kind === "session" && typeof event.sessionFile === "string" && event.sessionFile.length > 0) return event.sessionFile;
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function firstLineSessionFile(raw: string): string | undefined {
|
|
263
|
+
const newline = raw.indexOf("\n");
|
|
264
|
+
return parseSessionFileLine(newline >= 0 ? raw.slice(0, newline) : raw);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function chunkBySerializedBytes<T>(items: T[], wrap: (chunk: T[]) => unknown, maxBytes: number): T[][] {
|
|
268
|
+
const chunks: T[][] = [];
|
|
269
|
+
if (items.length === 0) return chunks;
|
|
270
|
+
const overhead = Buffer.byteLength(JSON.stringify(wrap([])), "utf-8");
|
|
271
|
+
let chunk: T[] = [];
|
|
272
|
+
let bytes = overhead;
|
|
273
|
+
for (const item of items) {
|
|
274
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf-8");
|
|
275
|
+
const nextBytes = bytes + itemBytes + (chunk.length > 0 ? 1 : 0);
|
|
276
|
+
if (chunk.length > 0 && nextBytes > maxBytes) {
|
|
277
|
+
chunks.push(chunk);
|
|
278
|
+
chunk = [item];
|
|
279
|
+
bytes = overhead + itemBytes;
|
|
280
|
+
} else {
|
|
281
|
+
chunk.push(item);
|
|
282
|
+
bytes = nextBytes;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
chunks.push(chunk);
|
|
286
|
+
return chunks;
|
|
287
|
+
}
|
|
288
|
+
|
|
249
289
|
export function buildCompactedLog(sessionFile: string, state: SessionState): string {
|
|
250
290
|
const byPath = new Map<string, Array<[string, string]>>();
|
|
251
291
|
for (const [anchor, entry] of state.owned) {
|
|
@@ -255,18 +295,18 @@ export function buildCompactedLog(sessionFile: string, state: SessionState): str
|
|
|
255
295
|
}
|
|
256
296
|
const out: string[] = [JSON.stringify({ kind: "session", sessionFile })];
|
|
257
297
|
for (const [path, rows] of byPath) {
|
|
258
|
-
for (
|
|
259
|
-
out.push(JSON.stringify({ kind: "allocate", path, rows:
|
|
298
|
+
for (const chunk of chunkBySerializedBytes(rows, (part) => ({ kind: "allocate", path, rows: part }), SIDECAR_COMPACT_LINE_BYTES)) {
|
|
299
|
+
out.push(JSON.stringify({ kind: "allocate", path, rows: chunk }));
|
|
260
300
|
}
|
|
261
301
|
}
|
|
262
302
|
const freedHistory = [...state.everMinted].filter((anchor) => !state.owned.has(anchor));
|
|
263
|
-
for (
|
|
264
|
-
out.push(JSON.stringify({ kind: "minted", anchors:
|
|
303
|
+
for (const chunk of chunkBySerializedBytes(freedHistory, (part) => ({ kind: "minted", anchors: part }), SIDECAR_COMPACT_LINE_BYTES)) {
|
|
304
|
+
out.push(JSON.stringify({ kind: "minted", anchors: chunk }));
|
|
265
305
|
}
|
|
266
306
|
return out.join("\n") + "\n";
|
|
267
307
|
}
|
|
268
|
-
async function compactSidecarIfNeeded(sidecar: string, raw: string, sessionFile: string, state: SessionState): Promise<void> {
|
|
269
|
-
if (!shouldCompactSidecar(raw)) return;
|
|
308
|
+
async function compactSidecarIfNeeded(sidecar: string, raw: string, sessionFile: string, state: SessionState, force = false): Promise<void> {
|
|
309
|
+
if (!force && !shouldCompactSidecar(raw)) return;
|
|
270
310
|
const tmp = `${sidecar}.compact-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
271
311
|
try {
|
|
272
312
|
const compacted = buildCompactedLog(sessionFile, state);
|
|
@@ -299,7 +339,7 @@ async function loadRegistryState(key: string, sessionFile: string, token: object
|
|
|
299
339
|
registries.set(key, folded);
|
|
300
340
|
sidecarByKey.set(key, sidecar);
|
|
301
341
|
if (rawLog.length > 0) {
|
|
302
|
-
await compactSidecarIfNeeded(sidecar, rawLog, sessionFile, folded);
|
|
342
|
+
await compactSidecarIfNeeded(sidecar, rawLog, sessionFile, folded, firstLineSessionFile(rawLog) !== sessionFile);
|
|
303
343
|
}
|
|
304
344
|
try {
|
|
305
345
|
await mkdir(sessionClaimsDir(), { recursive: true, mode: 0o700 });
|
|
@@ -476,6 +516,7 @@ export function clearRegistry(): void {
|
|
|
476
516
|
if (!state) return;
|
|
477
517
|
state.owned.clear();
|
|
478
518
|
state.served.clear();
|
|
519
|
+
state.allocatedChecksum.clear();
|
|
479
520
|
appendEvent({ kind: "clear" });
|
|
480
521
|
}
|
|
481
522
|
|
|
@@ -532,6 +573,7 @@ export function shadowStateFrom(state: SessionState): SessionState {
|
|
|
532
573
|
served: new Map(),
|
|
533
574
|
everMinted: new ShadowMintedSet(state.everMinted),
|
|
534
575
|
probe: state.probe,
|
|
576
|
+
allocatedChecksum: state.allocatedChecksum,
|
|
535
577
|
};
|
|
536
578
|
}
|
|
537
579
|
|
|
@@ -743,6 +785,11 @@ export function alignOwnership(
|
|
|
743
785
|
return { anchors, freed: freed.map((f) => f.anchor), minted: minted.map((m) => m.anchor) };
|
|
744
786
|
}
|
|
745
787
|
|
|
788
|
+
function snapshotMatchesAllocation(state: SessionState | undefined, path: string, checksum: string): boolean {
|
|
789
|
+
const allocated = state?.allocatedChecksum.get(path);
|
|
790
|
+
return allocated === undefined || allocated === checksum;
|
|
791
|
+
}
|
|
792
|
+
|
|
746
793
|
export async function allocateFileAnchors(
|
|
747
794
|
store: HashStore,
|
|
748
795
|
path: string,
|
|
@@ -754,12 +801,14 @@ export async function allocateFileAnchors(
|
|
|
754
801
|
},
|
|
755
802
|
): Promise<string[]> {
|
|
756
803
|
ensureRegistry();
|
|
804
|
+
const registry = current();
|
|
757
805
|
const shadow = options?.shadow === true;
|
|
758
806
|
const lines = splitLines(content);
|
|
759
807
|
const checksums = lines.map((line) => contentChecksum(hashSource(line)));
|
|
760
808
|
if (options?.previous?.spans) {
|
|
761
809
|
const prevChecksums = splitLines(options.previous.content).map((line) => contentChecksum(hashSource(line)));
|
|
762
810
|
const aligned = alignOwnershipWithSpans(path, options.previous.hashes, prevChecksums, checksums, options.previous.spans, { shadow });
|
|
811
|
+
if (!shadow && registry) registry.allocatedChecksum.set(path, contentChecksum(content));
|
|
763
812
|
if (!shadow && options.persist !== false) {
|
|
764
813
|
persistSnapshot(store, path, content, aligned.anchors, checksums);
|
|
765
814
|
}
|
|
@@ -772,7 +821,7 @@ export async function allocateFileAnchors(
|
|
|
772
821
|
prevChecksums = splitLines(options.previous.content).map((line) => contentChecksum(hashSource(line)));
|
|
773
822
|
} else {
|
|
774
823
|
const previousState = getAllocatedState(store, path, !shadow);
|
|
775
|
-
if (previousState) {
|
|
824
|
+
if (previousState && snapshotMatchesAllocation(registry, path, previousState.contentChecksum)) {
|
|
776
825
|
prevAnchors = previousState.anchors;
|
|
777
826
|
prevChecksums = previousState.checksums;
|
|
778
827
|
if (!prevChecksums && previousState.contentChecksum === contentChecksum(content)) {
|
|
@@ -800,6 +849,7 @@ export async function allocateFileAnchors(
|
|
|
800
849
|
}
|
|
801
850
|
return { anchors, freed: [], minted: anchors };
|
|
802
851
|
})();
|
|
852
|
+
if (!shadow && registry) registry.allocatedChecksum.set(path, contentChecksum(content));
|
|
803
853
|
if (!shadow && options?.persist !== false) {
|
|
804
854
|
persistSnapshot(store, path, content, aligned.anchors, checksums);
|
|
805
855
|
}
|
|
@@ -835,7 +885,7 @@ export function resetRegistryForTests(): void {
|
|
|
835
885
|
registries.clear();
|
|
836
886
|
}
|
|
837
887
|
|
|
838
|
-
|
|
888
|
+
async function readSidecarWindow(sidecar: string): Promise<{ text: string; filled: boolean }> {
|
|
839
889
|
const handle = await open(sidecar, "r");
|
|
840
890
|
const buffer = Buffer.alloc(SIDECAR_HEADER_BYTES);
|
|
841
891
|
try {
|
|
@@ -844,16 +894,31 @@ export async function readSidecarHeader(sidecar: string): Promise<string> {
|
|
|
844
894
|
const { bytesRead } = await handle.read(buffer, readBytes, Math.min(SIDECAR_HEADER_CHUNK, buffer.length - readBytes), readBytes);
|
|
845
895
|
if (bytesRead === 0) break;
|
|
846
896
|
readBytes += bytesRead;
|
|
847
|
-
const text = buffer.subarray(0, readBytes).toString("utf-8");
|
|
848
|
-
const newline = text.indexOf("\n");
|
|
849
|
-
if (newline >= 0) return text.slice(0, newline);
|
|
850
897
|
}
|
|
851
|
-
return buffer.subarray(0, readBytes).toString("utf-8");
|
|
898
|
+
return { text: buffer.subarray(0, readBytes).toString("utf-8"), filled: readBytes === buffer.length };
|
|
852
899
|
} finally {
|
|
853
900
|
await handle.close();
|
|
854
901
|
}
|
|
855
902
|
}
|
|
856
903
|
|
|
904
|
+
export async function readSidecarHeader(sidecar: string): Promise<string> {
|
|
905
|
+
const { text, filled } = await readSidecarWindow(sidecar);
|
|
906
|
+
const newline = text.indexOf("\n");
|
|
907
|
+
if (newline >= 0) return text.slice(0, newline);
|
|
908
|
+
return filled ? "" : text;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export async function readSidecarSessionFile(sidecar: string): Promise<string | undefined> {
|
|
912
|
+
const { text } = await readSidecarWindow(sidecar);
|
|
913
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
914
|
+
const complete = lastNewline >= 0 ? text.slice(0, lastNewline).split("\n") : [];
|
|
915
|
+
for (const line of complete) {
|
|
916
|
+
const sessionFile = parseSessionFileLine(line);
|
|
917
|
+
if (sessionFile !== undefined) return sessionFile;
|
|
918
|
+
}
|
|
919
|
+
return parseSessionFileLine(lastNewline >= 0 ? text.slice(lastNewline + 1) : text);
|
|
920
|
+
}
|
|
921
|
+
|
|
857
922
|
export function releaseRegistrySession(key: string): void {
|
|
858
923
|
loadTokens.delete(key);
|
|
859
924
|
if (currentKey === key) currentKey = undefined;
|
|
@@ -884,9 +949,9 @@ export async function gcRegistrySidecars(): Promise<void> {
|
|
|
884
949
|
if (!name.endsWith(SIDECAR_SUFFIX)) continue;
|
|
885
950
|
const sidecar = join(sessionClaimsDir(), name);
|
|
886
951
|
try {
|
|
887
|
-
const
|
|
888
|
-
if (
|
|
889
|
-
await stat(
|
|
952
|
+
const sessionFile = await readSidecarSessionFile(sidecar);
|
|
953
|
+
if (sessionFile === undefined) continue;
|
|
954
|
+
await stat(sessionFile);
|
|
890
955
|
} catch (error) {
|
|
891
956
|
if (errCode(error) === "ENOENT") {
|
|
892
957
|
await rm(sidecar, { force: true });
|
package/src/fs-write.ts
CHANGED
|
@@ -152,6 +152,7 @@ export async function writeAtomic(
|
|
|
152
152
|
path: string,
|
|
153
153
|
content: string,
|
|
154
154
|
expectedIdentity?: FileIdentity,
|
|
155
|
+
restoreMode?: number,
|
|
155
156
|
): Promise<void> {
|
|
156
157
|
const targetPath = await resolveTarget(path);
|
|
157
158
|
|
|
@@ -193,8 +194,9 @@ export async function writeAtomic(
|
|
|
193
194
|
const tempHandle = await open(tempPath, "wx", 0o600);
|
|
194
195
|
try {
|
|
195
196
|
await tempHandle.writeFile(content, "utf-8");
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
const mode = existingStats ? existingStats.mode & 0o7777 : restoreMode;
|
|
198
|
+
if (mode !== undefined) {
|
|
199
|
+
await tempHandle.chmod(mode);
|
|
198
200
|
}
|
|
199
201
|
await tempHandle.sync();
|
|
200
202
|
} catch (error: unknown) {
|
package/src/hash-store/retry.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function withBusyRetryAsync<T>(fn: () => T): Promise<T> {
|
|
|
37
37
|
throw lastError;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function retriedWrite(stmt: { run(...params: (string | number)[]): unknown }): (...params: (string | number)[]) => void {
|
|
40
|
+
export function retriedWrite(stmt: { run(...params: (string | number | null)[]): unknown }): (...params: (string | number | null)[]) => void {
|
|
41
41
|
return (...params) => {
|
|
42
42
|
withBusyRetry(() => { stmt.run(...params); });
|
|
43
43
|
};
|
package/src/hash-store.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { isValidHashList, isValidServedMap, parseHashList, parseServedMap, parse
|
|
|
26
26
|
export { SNAPSHOT_CACHE_LIMIT };
|
|
27
27
|
export const STORE_NOT_OPEN_MESSAGE = "Hash store is not open; transactional update aborted";
|
|
28
28
|
|
|
29
|
-
type SqlParams = (string | number)[];
|
|
29
|
+
type SqlParams = (string | number | null)[];
|
|
30
30
|
|
|
31
31
|
interface RawStatement {
|
|
32
32
|
get(...params: SqlParams): unknown;
|
|
@@ -140,6 +140,7 @@ export interface UndoRecord {
|
|
|
140
140
|
ending: string;
|
|
141
141
|
hashes: string[];
|
|
142
142
|
resultContent: string;
|
|
143
|
+
mode?: number;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
let cachedDb: { path: string; db: RawDb; stmts: Prepared } | null = null;
|
|
@@ -190,6 +191,9 @@ function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
|
190
191
|
try {
|
|
191
192
|
db.exec("ALTER TABLE snapshots ADD COLUMN line_checksums TEXT");
|
|
192
193
|
} catch {}
|
|
194
|
+
try {
|
|
195
|
+
db.exec("ALTER TABLE undo ADD COLUMN mode INTEGER");
|
|
196
|
+
} catch {}
|
|
193
197
|
const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'version'").get() as { value?: string } | undefined;
|
|
194
198
|
if (versionRow && versionRow.value !== String(HASH_STORE_VERSION)) {
|
|
195
199
|
db.exec("DELETE FROM snapshots");
|
|
@@ -209,11 +213,11 @@ function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
|
209
213
|
"ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, line_checksums = excluded.line_checksums, updated_at = excluded.updated_at"
|
|
210
214
|
);
|
|
211
215
|
const undoUpsertStmt = db.prepare(
|
|
212
|
-
"INSERT INTO undo (path, content, bom, ending, hashes, result_content, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) " +
|
|
213
|
-
"ON CONFLICT(path) DO UPDATE SET content = excluded.content, bom = excluded.bom, ending = excluded.ending, hashes = excluded.hashes, result_content = excluded.result_content, updated_at = excluded.updated_at"
|
|
216
|
+
"INSERT INTO undo (path, content, bom, ending, hashes, result_content, mode, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) " +
|
|
217
|
+
"ON CONFLICT(path) DO UPDATE SET content = excluded.content, bom = excluded.bom, ending = excluded.ending, hashes = excluded.hashes, result_content = excluded.result_content, mode = excluded.mode, updated_at = excluded.updated_at"
|
|
214
218
|
);
|
|
215
219
|
const undoGetStmt = db.prepare(
|
|
216
|
-
"SELECT content, bom, ending, hashes, result_content FROM undo WHERE path = ?"
|
|
220
|
+
"SELECT content, bom, ending, hashes, result_content, mode FROM undo WHERE path = ?"
|
|
217
221
|
);
|
|
218
222
|
const undoDelStmt = db.prepare("DELETE FROM undo WHERE path = ?");
|
|
219
223
|
const snapshotTimeStmt = db.prepare("SELECT updated_at FROM snapshots WHERE path = ?");
|
|
@@ -533,6 +537,7 @@ export function upsertUndo(store: HashStore, path: string, entry: UndoRecord): v
|
|
|
533
537
|
entry.ending,
|
|
534
538
|
JSON.stringify(entry.hashes),
|
|
535
539
|
entry.resultContent,
|
|
540
|
+
typeof entry.mode === "number" ? entry.mode : null,
|
|
536
541
|
Date.now(),
|
|
537
542
|
);
|
|
538
543
|
touchSession(path);
|
|
@@ -549,6 +554,7 @@ export function getUndoEntry(store: HashStore, path: string): UndoRecord | undef
|
|
|
549
554
|
ending: row.ending as string,
|
|
550
555
|
hashes: parsed,
|
|
551
556
|
resultContent: row.result_content as string,
|
|
557
|
+
...(typeof row.mode === "number" ? { mode: row.mode } : {}),
|
|
552
558
|
};
|
|
553
559
|
}
|
|
554
560
|
|
package/src/replace-undo.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { open } from "node:fs/promises";
|
|
2
|
+
import { open, stat } from "node:fs/promises";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Type } from "typebox";
|
|
@@ -24,6 +24,7 @@ export interface UndoEntry {
|
|
|
24
24
|
originalEnding: LineEnding;
|
|
25
25
|
hashes: string[];
|
|
26
26
|
resultContent: string;
|
|
27
|
+
mode?: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export async function saveUndo(
|
|
@@ -34,12 +35,18 @@ export async function saveUndo(
|
|
|
34
35
|
try {
|
|
35
36
|
const store = await loadHashStore();
|
|
36
37
|
previous = getUndoEntry(store, path);
|
|
38
|
+
let mode: number | undefined;
|
|
39
|
+
try {
|
|
40
|
+
mode = (await stat(path)).mode & 0o7777;
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
37
43
|
upsertUndo(store, path, {
|
|
38
44
|
content: entry.content,
|
|
39
45
|
bom: entry.bom,
|
|
40
46
|
ending: entry.originalEnding,
|
|
41
47
|
hashes: entry.hashes,
|
|
42
48
|
resultContent: entry.resultContent,
|
|
49
|
+
...(mode !== undefined ? { mode } : {}),
|
|
43
50
|
});
|
|
44
51
|
} catch (error) {
|
|
45
52
|
console.error("Failed to persist undo entry:", error);
|
|
@@ -75,6 +82,7 @@ export async function getUndo(path: string): Promise<UndoEntry | undefined> {
|
|
|
75
82
|
originalEnding,
|
|
76
83
|
hashes: record.hashes,
|
|
77
84
|
resultContent: record.resultContent,
|
|
85
|
+
...(record.mode !== undefined ? { mode: record.mode } : {}),
|
|
78
86
|
};
|
|
79
87
|
} catch (error) {
|
|
80
88
|
console.error("Failed to load undo entry:", error);
|
|
@@ -91,6 +99,10 @@ export async function clearUndo(path: string): Promise<void> {
|
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
function fallbackFileMode(): number {
|
|
103
|
+
return 0o666 & ~process.umask();
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
export function regUndo(pi: ExtensionAPI): void {
|
|
95
107
|
pi.registerTool({
|
|
96
108
|
name: "undo_last_change",
|
|
@@ -172,6 +184,7 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
172
184
|
mutationTargetPath,
|
|
173
185
|
undo.bom + restoreEndings(undo.content, undo.originalEnding),
|
|
174
186
|
currentIdentity,
|
|
187
|
+
undo.mode ?? fallbackFileMode(),
|
|
175
188
|
);
|
|
176
189
|
|
|
177
190
|
const currentNormalized = currentRaw === undefined ? "" : toLF(stripBOM(currentRaw).text);
|