@shanepadgett/tau-agent 0.15.0 → 0.17.0
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/docs/subagents.md +25 -6
- package/extensions/attention/README.md +1 -0
- package/extensions/attention/index.ts +38 -0
- package/extensions/context/README.md +13 -3
- package/extensions/context/definitions.ts +3 -15
- package/extensions/context/evidence.ts +516 -0
- package/extensions/context/index.ts +114 -116
- package/extensions/context/panel.ts +60 -0
- package/extensions/context/settings.ts +30 -1
- package/extensions/context/sync.ts +171 -686
- package/extensions/context/validation.ts +4 -1
- package/extensions/context/write-scope.ts +109 -0
- package/extensions/context-pruning/README.md +22 -0
- package/extensions/context-pruning/file-evidence.ts +265 -0
- package/extensions/context-pruning/index.ts +379 -0
- package/extensions/context-pruning/projection.ts +108 -0
- package/extensions/context-pruning/prune.ts +346 -0
- package/extensions/context-pruning/render.ts +179 -0
- package/extensions/context-pruning/settings.ts +41 -0
- package/extensions/explore/README.md +3 -1
- package/extensions/explore/autoread.ts +85 -22
- package/extensions/explore/full-file-knowledge.ts +234 -0
- package/extensions/explore/index.ts +2 -3
- package/extensions/explore/read-cache.ts +150 -86
- package/extensions/explore/read-snapshots.ts +17 -4
- package/extensions/explore/read.ts +57 -38
- package/extensions/footer/index.ts +62 -60
- package/extensions/patch/README.md +1 -1
- package/extensions/patch/index.ts +19 -5
- package/extensions/run-summary/index.ts +5 -5
- package/extensions/silent-command-runner/README.md +1 -1
- package/extensions/silent-command-runner/index.ts +45 -28
- package/extensions/soul/prompt.ts +3 -1
- package/extensions/subagent/README.md +21 -5
- package/extensions/subagent/agents/context-sync.md +90 -0
- package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
- package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
- package/extensions/subagent/agents/review.md +48 -0
- package/extensions/subagent/agents/web-research.md +6 -0
- package/extensions/subagent/agents.ts +15 -2
- package/extensions/subagent/cmux-dashboard.ts +454 -0
- package/extensions/subagent/index.ts +181 -234
- package/extensions/subagent/render.ts +1 -1
- package/extensions/subagent/resume.ts +78 -0
- package/extensions/subagent/run.ts +213 -118
- package/extensions/subagent/runtime.ts +856 -0
- package/extensions/subagent/session-resource.ts +169 -0
- package/extensions/tau-help/help.md +7 -3
- package/extensions/turn-budget/index.ts +8 -36
- package/package.json +2 -2
- package/schemas/tau.schema.json +48 -1
- package/shared/context-pruning-state.ts +364 -0
- package/shared/events.ts +18 -0
- package/shared/model-fallback/index.ts +21 -10
- package/shared/model-fallback/types.ts +5 -3
- package/shared/settings/load.ts +78 -1
- package/shared/tool-row-state.ts +21 -1
|
@@ -5,13 +5,14 @@ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import { onTauEvent, type TauAgentEvents } from "../../shared/events.js";
|
|
6
6
|
import type { ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
7
7
|
import { Marker, type MarkerState } from "@shanepadgett/tau-tui";
|
|
8
|
+
import { createCompleteFileMeta } from "./full-file-knowledge.ts";
|
|
8
9
|
import type { ReadCacheMetaV1 } from "./read-cache.ts";
|
|
9
10
|
|
|
10
11
|
const AUTOREAD_MESSAGE_TYPE = "tau.autoread";
|
|
11
12
|
|
|
12
13
|
type AutoreadStatus = "reading" | "read" | "failed";
|
|
13
14
|
|
|
14
|
-
interface AutoreadDetails {
|
|
15
|
+
export interface AutoreadDetails {
|
|
15
16
|
rowId: string;
|
|
16
17
|
path: string;
|
|
17
18
|
cwd: string;
|
|
@@ -22,10 +23,77 @@ interface AutoreadDetails {
|
|
|
22
23
|
readCache?: ReadCacheMetaV1;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export interface PreparedAutoreadMessage {
|
|
27
|
+
customType: typeof AUTOREAD_MESSAGE_TYPE;
|
|
28
|
+
content: string;
|
|
29
|
+
display: true;
|
|
30
|
+
details: AutoreadDetails & { status: "read"; readCache: ReadCacheMetaV1 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function prepareAutoreadMessage(options: {
|
|
34
|
+
rowId: string;
|
|
35
|
+
path: string;
|
|
36
|
+
cwd: string;
|
|
37
|
+
source: string;
|
|
38
|
+
batchId: string;
|
|
39
|
+
signal: AbortSignal | undefined;
|
|
40
|
+
isLifecycleCurrent: () => boolean;
|
|
41
|
+
maximumBytes?: number;
|
|
42
|
+
}): Promise<PreparedAutoreadMessage> {
|
|
43
|
+
assertPreparationCurrent(options.signal, options.isLifecycleCurrent);
|
|
44
|
+
const pathKey = resolve(options.cwd, options.path);
|
|
45
|
+
const bytes = options.signal ? await readFile(pathKey, { signal: options.signal }) : await readFile(pathKey);
|
|
46
|
+
if (options.maximumBytes !== undefined && bytes.byteLength > options.maximumBytes) {
|
|
47
|
+
throw new Error(`File exceeds the ${options.maximumBytes}-byte complete-file snapshot limit`);
|
|
48
|
+
}
|
|
49
|
+
const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
50
|
+
const messageContent = `${options.path}\n${content}`;
|
|
51
|
+
const totalLines = content.split("\n").length;
|
|
52
|
+
const readCache = createCompleteFileMeta({
|
|
53
|
+
pathKey,
|
|
54
|
+
presentation: "plain",
|
|
55
|
+
servedHash: createHash("sha256").update(content, "utf8").digest("hex"),
|
|
56
|
+
mode: "baseline",
|
|
57
|
+
sourceText: content,
|
|
58
|
+
returnedText: messageContent,
|
|
59
|
+
totalLines,
|
|
60
|
+
summary: `${totalLines} lines`,
|
|
61
|
+
}) satisfies ReadCacheMetaV1;
|
|
62
|
+
assertPreparationCurrent(options.signal, options.isLifecycleCurrent);
|
|
63
|
+
return {
|
|
64
|
+
customType: AUTOREAD_MESSAGE_TYPE,
|
|
65
|
+
content: messageContent,
|
|
66
|
+
display: true,
|
|
67
|
+
details: {
|
|
68
|
+
rowId: options.rowId,
|
|
69
|
+
path: options.path,
|
|
70
|
+
cwd: options.cwd,
|
|
71
|
+
source: options.source,
|
|
72
|
+
batchId: options.batchId,
|
|
73
|
+
status: "read",
|
|
74
|
+
readCache,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
25
79
|
export function registerAutoread(pi: ExtensionAPI, rowState: ToolRowStateStore): void {
|
|
80
|
+
let lifecycleGeneration = 0;
|
|
81
|
+
pi.on("session_start", () => {
|
|
82
|
+
lifecycleGeneration += 1;
|
|
83
|
+
});
|
|
84
|
+
pi.on("session_compact", () => {
|
|
85
|
+
lifecycleGeneration += 1;
|
|
86
|
+
});
|
|
87
|
+
pi.on("session_tree", () => {
|
|
88
|
+
lifecycleGeneration += 1;
|
|
89
|
+
});
|
|
90
|
+
pi.on("session_shutdown", () => {
|
|
91
|
+
lifecycleGeneration += 1;
|
|
92
|
+
});
|
|
26
93
|
onTauEvent(pi, "explore.autoread", "tau:autoread.requested", async (data) => {
|
|
27
94
|
const event = readAutoreadRequestedEvent(data);
|
|
28
95
|
if (!event) return;
|
|
96
|
+
const generation = lifecycleGeneration;
|
|
29
97
|
await Promise.all(
|
|
30
98
|
event.files.map(async (file, index) => {
|
|
31
99
|
const rowId = `${event.batchId}:${index}`;
|
|
@@ -37,28 +105,14 @@ export function registerAutoread(pi: ExtensionAPI, rowState: ToolRowStateStore):
|
|
|
37
105
|
batchId: event.batchId,
|
|
38
106
|
} satisfies Omit<AutoreadDetails, "status" | "error">;
|
|
39
107
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const readCache = {
|
|
45
|
-
v: 1,
|
|
46
|
-
pathKey,
|
|
47
|
-
scopeKey: "full:n0",
|
|
48
|
-
servedHash: createHash("sha256").update(bytes).digest("hex"),
|
|
49
|
-
mode: "baseline",
|
|
50
|
-
baselineTokens: Math.ceil(content.length / 4),
|
|
51
|
-
returnedTokens: Math.ceil(content.length / 4),
|
|
52
|
-
totalLines,
|
|
53
|
-
summary: `${totalLines} lines`,
|
|
54
|
-
} satisfies ReadCacheMetaV1;
|
|
55
|
-
pi.sendMessage({
|
|
56
|
-
customType: AUTOREAD_MESSAGE_TYPE,
|
|
57
|
-
content: `${file.path}\n${content}`,
|
|
58
|
-
display: true,
|
|
59
|
-
details: { ...details, status: "read", readCache },
|
|
108
|
+
const message = await prepareAutoreadMessage({
|
|
109
|
+
...details,
|
|
110
|
+
signal: undefined,
|
|
111
|
+
isLifecycleCurrent: () => generation === lifecycleGeneration,
|
|
60
112
|
});
|
|
113
|
+
pi.sendMessage(message);
|
|
61
114
|
} catch (error) {
|
|
115
|
+
if (generation !== lifecycleGeneration) return;
|
|
62
116
|
const message = error instanceof Error ? error.message : String(error);
|
|
63
117
|
pi.sendMessage({
|
|
64
118
|
customType: AUTOREAD_MESSAGE_TYPE,
|
|
@@ -78,6 +132,11 @@ export function registerAutoread(pi: ExtensionAPI, rowState: ToolRowStateStore):
|
|
|
78
132
|
});
|
|
79
133
|
}
|
|
80
134
|
|
|
135
|
+
function assertPreparationCurrent(signal: AbortSignal | undefined, isLifecycleCurrent: () => boolean): void {
|
|
136
|
+
signal?.throwIfAborted();
|
|
137
|
+
if (!isLifecycleCurrent()) throw new Error("Autoread preparation crossed a session lifecycle boundary");
|
|
138
|
+
}
|
|
139
|
+
|
|
81
140
|
function readAutoreadRequestedEvent(value: unknown): TauAgentEvents["tau:autoread.requested"] | undefined {
|
|
82
141
|
if (!value || typeof value !== "object") return undefined;
|
|
83
142
|
const record = value as Record<string, unknown>;
|
|
@@ -136,6 +195,7 @@ class AutoreadMessageComponent {
|
|
|
136
195
|
this.path = path;
|
|
137
196
|
this.status = status;
|
|
138
197
|
this.theme = theme;
|
|
198
|
+
this.rowState.watch(this.rowId, () => this.invalidate());
|
|
139
199
|
}
|
|
140
200
|
|
|
141
201
|
render(width: number): string[] {
|
|
@@ -147,7 +207,10 @@ class AutoreadMessageComponent {
|
|
|
147
207
|
}).render(width);
|
|
148
208
|
}
|
|
149
209
|
|
|
150
|
-
invalidate(): void {
|
|
210
|
+
invalidate(): void {
|
|
211
|
+
// Rendering is stateless; this watcher gives the owning custom-message row
|
|
212
|
+
// the invalidation signal needed to redraw after a row-state snapshot.
|
|
213
|
+
}
|
|
151
214
|
|
|
152
215
|
private markerState(): MarkerState {
|
|
153
216
|
if (this.rowState.get(this.rowId) === "pruned") return "warning";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { generateUnifiedPatch, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ReadCacheMetaV1, ReadCacheMode, ReadCachePresentation } from "./read-cache.ts";
|
|
4
|
+
|
|
5
|
+
export const COMPLETE_FILE_SCOPE = "full";
|
|
6
|
+
export const MAX_COMPLETE_FILE_SNAPSHOT_BYTES = 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
export interface CompleteFileSelection {
|
|
9
|
+
text: string;
|
|
10
|
+
mode: ReadCacheMode;
|
|
11
|
+
summary: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createCompleteFileMeta(options: {
|
|
15
|
+
pathKey: string;
|
|
16
|
+
presentation: ReadCachePresentation;
|
|
17
|
+
servedHash: string;
|
|
18
|
+
mode: ReadCacheMode;
|
|
19
|
+
sourceText: string;
|
|
20
|
+
returnedText: string;
|
|
21
|
+
totalLines: number;
|
|
22
|
+
summary: string;
|
|
23
|
+
baseHash?: string;
|
|
24
|
+
}): ReadCacheMetaV1 {
|
|
25
|
+
return {
|
|
26
|
+
v: 1,
|
|
27
|
+
pathKey: options.pathKey,
|
|
28
|
+
scopeKey: COMPLETE_FILE_SCOPE,
|
|
29
|
+
presentation: options.presentation,
|
|
30
|
+
servedHash: options.servedHash,
|
|
31
|
+
baseHash: options.baseHash,
|
|
32
|
+
mode: options.mode,
|
|
33
|
+
baselineTokens: estimateTokens(options.sourceText),
|
|
34
|
+
returnedTokens: estimateTokens(options.returnedText),
|
|
35
|
+
totalLines: options.totalLines,
|
|
36
|
+
summary: options.summary,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readScopeKey(
|
|
41
|
+
completeFile: boolean,
|
|
42
|
+
startLine: number,
|
|
43
|
+
endLine: number,
|
|
44
|
+
presentation: ReadCachePresentation,
|
|
45
|
+
): string {
|
|
46
|
+
return completeFile ? COMPLETE_FILE_SCOPE : `r:${startLine}:${endLine}:n${presentation === "line-numbered" ? 1 : 0}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function decodeCompleteFileSource(options: {
|
|
50
|
+
content: unknown;
|
|
51
|
+
autoread: boolean;
|
|
52
|
+
pathHeader?: string;
|
|
53
|
+
presentation: ReadCachePresentation;
|
|
54
|
+
servedHash: string;
|
|
55
|
+
}): { valid: boolean; text?: string } {
|
|
56
|
+
const payload = textContent(options.content);
|
|
57
|
+
if (payload === undefined) return { valid: false };
|
|
58
|
+
const separator = payload.indexOf("\n");
|
|
59
|
+
if (
|
|
60
|
+
options.autoread &&
|
|
61
|
+
(separator < 0 || options.pathHeader === undefined || payload.slice(0, separator) !== options.pathHeader)
|
|
62
|
+
)
|
|
63
|
+
return { valid: false };
|
|
64
|
+
const body = options.autoread ? payload.slice(separator + 1) : payload;
|
|
65
|
+
const text = options.presentation === "plain" ? body : decodeLineNumbers(body);
|
|
66
|
+
if (text === undefined) return { valid: false };
|
|
67
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
68
|
+
if (createHash("sha256").update(text, "utf8").digest("hex") !== options.servedHash) return { valid: false };
|
|
69
|
+
return bytes <= MAX_COMPLETE_FILE_SNAPSHOT_BYTES ? { valid: true, text } : { valid: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function applyCompleteFileDiff(options: {
|
|
73
|
+
content: unknown;
|
|
74
|
+
baseText: string;
|
|
75
|
+
baseHash: string;
|
|
76
|
+
servedHash: string;
|
|
77
|
+
}): { valid: boolean; text?: string } {
|
|
78
|
+
if (hashText(options.baseText) !== options.baseHash) return { valid: false };
|
|
79
|
+
const payload = textContent(options.content);
|
|
80
|
+
if (payload === undefined) return { valid: false };
|
|
81
|
+
const separator = payload.indexOf("\n");
|
|
82
|
+
if (separator < 0 || !/^\[read: \d+ lines added, \d+ removed of \d+\]$/.test(payload.slice(0, separator))) {
|
|
83
|
+
return { valid: false };
|
|
84
|
+
}
|
|
85
|
+
const text = applyUnifiedDiff(options.baseText, payload.slice(separator + 1));
|
|
86
|
+
if (text === undefined || hashText(text) !== options.servedHash) return { valid: false };
|
|
87
|
+
return Buffer.byteLength(text, "utf8") <= MAX_COMPLETE_FILE_SNAPSHOT_BYTES ? { valid: true, text } : { valid: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function selectCompleteFileResponse(options: {
|
|
91
|
+
displayPath: string;
|
|
92
|
+
currentText: string;
|
|
93
|
+
currentHash: string;
|
|
94
|
+
fullText: string;
|
|
95
|
+
totalLines: number;
|
|
96
|
+
recovery: boolean;
|
|
97
|
+
baseHash?: string;
|
|
98
|
+
baselineText?: string;
|
|
99
|
+
}): CompleteFileSelection {
|
|
100
|
+
if (options.recovery) return { text: options.fullText, mode: "recovery", summary: `${options.totalLines} lines` };
|
|
101
|
+
if (!options.baseHash) return { text: options.fullText, mode: "baseline", summary: `${options.totalLines} lines` };
|
|
102
|
+
if (options.baseHash === options.currentHash) {
|
|
103
|
+
const text = `unchanged, ${options.totalLines} lines`;
|
|
104
|
+
return { text, mode: "unchanged", summary: text };
|
|
105
|
+
}
|
|
106
|
+
if (options.baselineText !== undefined) {
|
|
107
|
+
const patch = generateUnifiedPatch(options.displayPath, options.baselineText, options.currentText, 3);
|
|
108
|
+
const counts = countDiffLines(patch);
|
|
109
|
+
const candidate = `[read: ${counts.added} lines added, ${counts.removed} removed of ${options.totalLines}]\n${patch}`;
|
|
110
|
+
if (!truncateHead(candidate).truncated && estimateTokens(candidate) < estimateTokens(options.fullText)) {
|
|
111
|
+
return { text: candidate, mode: "diff", summary: `+${counts.added} -${counts.removed}` };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { text: options.fullText, mode: "baseline", summary: `${options.totalLines} lines` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function estimateTokens(text: string): number {
|
|
118
|
+
return Math.ceil(text.length / 4);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function textContent(content: unknown): string | undefined {
|
|
122
|
+
if (typeof content === "string") return content;
|
|
123
|
+
if (!Array.isArray(content) || content.length !== 1) return undefined;
|
|
124
|
+
const part = content[0];
|
|
125
|
+
if (!part || typeof part !== "object") return undefined;
|
|
126
|
+
const record = part as Record<string, unknown>;
|
|
127
|
+
return record.type === "text" && typeof record.text === "string" ? record.text : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function applyUnifiedDiff(source: string, patch: string): string | undefined {
|
|
131
|
+
const patchLines = patch.split("\n");
|
|
132
|
+
const oldHeaderIndex = patchLines.findIndex(
|
|
133
|
+
(line, index) => line.startsWith("--- ") && patchLines[index + 1]?.startsWith("+++ "),
|
|
134
|
+
);
|
|
135
|
+
if (oldHeaderIndex < 0) return undefined;
|
|
136
|
+
const sourceEndsWithNewline = source.endsWith("\n");
|
|
137
|
+
const sourceLines = source === "" ? [] : source.split("\n");
|
|
138
|
+
if (sourceEndsWithNewline) sourceLines.pop();
|
|
139
|
+
const output: string[] = [];
|
|
140
|
+
let sourceIndex = 0;
|
|
141
|
+
let patchIndex = oldHeaderIndex + 2;
|
|
142
|
+
let hunkCount = 0;
|
|
143
|
+
let outputEndsWithNewline = sourceEndsWithNewline;
|
|
144
|
+
|
|
145
|
+
while (patchIndex < patchLines.length) {
|
|
146
|
+
const header = patchLines[patchIndex];
|
|
147
|
+
if (header === "" && patchIndex === patchLines.length - 1) break;
|
|
148
|
+
if (header === undefined) return undefined;
|
|
149
|
+
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$/.exec(header);
|
|
150
|
+
if (!match) return undefined;
|
|
151
|
+
const oldStartValue = Number(match[1]);
|
|
152
|
+
const oldCount = match[2] === undefined ? 1 : Number(match[2]);
|
|
153
|
+
const newCount = match[4] === undefined ? 1 : Number(match[4]);
|
|
154
|
+
if (!Number.isSafeInteger(oldStartValue) || !Number.isSafeInteger(oldCount) || !Number.isSafeInteger(newCount)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const oldStart = oldCount === 0 ? oldStartValue : oldStartValue - 1;
|
|
158
|
+
if (oldStart < sourceIndex || oldStart > sourceLines.length) return undefined;
|
|
159
|
+
output.push(...sourceLines.slice(sourceIndex, oldStart));
|
|
160
|
+
sourceIndex = oldStart;
|
|
161
|
+
patchIndex += 1;
|
|
162
|
+
let consumedOld = 0;
|
|
163
|
+
let producedNew = 0;
|
|
164
|
+
let previousMarker: string | undefined;
|
|
165
|
+
let newSideHasNoFinalNewline = false;
|
|
166
|
+
|
|
167
|
+
while (patchIndex < patchLines.length && !patchLines[patchIndex]?.startsWith("@@ ")) {
|
|
168
|
+
const line = patchLines[patchIndex];
|
|
169
|
+
if (line === "" && patchIndex === patchLines.length - 1) break;
|
|
170
|
+
if (line === undefined) return undefined;
|
|
171
|
+
if (line === "\") {
|
|
172
|
+
if (previousMarker !== "+" && previousMarker !== "-" && previousMarker !== " ") return undefined;
|
|
173
|
+
if (previousMarker === "+" || previousMarker === " ") newSideHasNoFinalNewline = true;
|
|
174
|
+
patchIndex += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const marker = line[0];
|
|
178
|
+
const value = line.slice(1);
|
|
179
|
+
if (marker === " ") {
|
|
180
|
+
if (sourceLines[sourceIndex] !== value) return undefined;
|
|
181
|
+
output.push(value);
|
|
182
|
+
sourceIndex += 1;
|
|
183
|
+
consumedOld += 1;
|
|
184
|
+
producedNew += 1;
|
|
185
|
+
} else if (marker === "-") {
|
|
186
|
+
if (sourceLines[sourceIndex] !== value) return undefined;
|
|
187
|
+
sourceIndex += 1;
|
|
188
|
+
consumedOld += 1;
|
|
189
|
+
} else if (marker === "+") {
|
|
190
|
+
output.push(value);
|
|
191
|
+
producedNew += 1;
|
|
192
|
+
} else {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
previousMarker = marker;
|
|
196
|
+
patchIndex += 1;
|
|
197
|
+
}
|
|
198
|
+
if (consumedOld !== oldCount || producedNew !== newCount) return undefined;
|
|
199
|
+
if (sourceIndex === sourceLines.length) {
|
|
200
|
+
outputEndsWithNewline = newCount > 0 && !newSideHasNoFinalNewline;
|
|
201
|
+
}
|
|
202
|
+
hunkCount += 1;
|
|
203
|
+
}
|
|
204
|
+
if (hunkCount === 0) return undefined;
|
|
205
|
+
output.push(...sourceLines.slice(sourceIndex));
|
|
206
|
+
const text = output.join("\n");
|
|
207
|
+
return outputEndsWithNewline && output.length > 0 ? `${text}\n` : text;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hashText(text: string): string {
|
|
211
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function decodeLineNumbers(content: string): string | undefined {
|
|
215
|
+
const lines = content.split("\n");
|
|
216
|
+
const decoded: string[] = [];
|
|
217
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
218
|
+
const prefix = `${index + 1}: `;
|
|
219
|
+
const line = lines[index];
|
|
220
|
+
if (line === undefined || !line.startsWith(prefix)) return undefined;
|
|
221
|
+
decoded.push(line.slice(prefix.length));
|
|
222
|
+
}
|
|
223
|
+
return decoded.join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function countDiffLines(patch: string): { added: number; removed: number } {
|
|
227
|
+
let added = 0;
|
|
228
|
+
let removed = 0;
|
|
229
|
+
for (const line of patch.split("\n")) {
|
|
230
|
+
if (line.startsWith("+") && !line.startsWith("+++")) added += 1;
|
|
231
|
+
else if (line.startsWith("-") && !line.startsWith("---")) removed += 1;
|
|
232
|
+
}
|
|
233
|
+
return { added, removed };
|
|
234
|
+
}
|
|
@@ -30,9 +30,8 @@ export default function exploreExtension(pi: ExtensionAPI): void {
|
|
|
30
30
|
});
|
|
31
31
|
pi.on("session_start", () => {
|
|
32
32
|
rowState.clear();
|
|
33
|
-
readCache.clear();
|
|
34
33
|
readSnapshots.clear();
|
|
35
34
|
});
|
|
36
|
-
pi.on("session_compact", () =>
|
|
37
|
-
pi.on("session_tree", () =>
|
|
35
|
+
pi.on("session_compact", () => readSnapshots.clear());
|
|
36
|
+
pi.on("session_tree", () => readSnapshots.clear());
|
|
38
37
|
}
|