@oh-my-pi/pi-coding-agent 14.6.6 → 14.7.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/CHANGELOG.md +41 -0
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/03-custom-prompt.ts +7 -4
- package/examples/sdk/README.md +1 -1
- package/package.json +7 -7
- package/src/autoresearch/index.ts +48 -44
- package/src/cli/read-cli.ts +58 -0
- package/src/cli.ts +1 -0
- package/src/commands/read.ts +40 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/analysis/conventional.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/map-reduce/map-phase.ts +1 -1
- package/src/commit/map-reduce/reduce-phase.ts +1 -1
- package/src/config/settings-schema.ts +39 -0
- package/src/edit/line-hash.ts +34 -4
- package/src/edit/modes/hashline.ts +201 -6
- package/src/edit/streaming.ts +4 -1
- package/src/export/html/index.ts +1 -1
- package/src/extensibility/extensions/runner.ts +3 -3
- package/src/extensibility/extensions/types.ts +4 -4
- package/src/main.ts +3 -3
- package/src/memories/index.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/read-tool-group.ts +4 -9
- package/src/modes/components/tool-execution.ts +4 -0
- package/src/modes/controllers/event-controller.ts +2 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/utils/context-usage.ts +12 -5
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/project-prompt.md +36 -0
- package/src/prompts/system/system-prompt.md +0 -29
- package/src/prompts/tools/github.md +1 -0
- package/src/prompts/tools/read.md +15 -14
- package/src/sdk.ts +29 -28
- package/src/session/agent-session.ts +20 -12
- package/src/session/compaction/branch-summarization.ts +1 -1
- package/src/session/compaction/compaction.ts +3 -3
- package/src/session/session-dump-format.ts +10 -5
- package/src/session/streaming-output.ts +1 -1
- package/src/system-prompt.ts +35 -3
- package/src/task/executor.ts +4 -3
- package/src/tools/fetch.ts +4 -4
- package/src/tools/gh.ts +187 -0
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/path-utils.ts +11 -0
- package/src/tools/read.ts +388 -204
- package/src/tools/search.ts +1 -1
- package/src/tools/sqlite-reader.ts +1 -1
- package/src/utils/commit-message-generator.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/workspace-tree.ts +396 -0
|
@@ -103,6 +103,9 @@ export interface CompactHashlineDiffOptions {
|
|
|
103
103
|
/** Maximum entries kept on each side of an unchanged-context truncation (default: 2). */
|
|
104
104
|
maxUnchangedRun?: number;
|
|
105
105
|
}
|
|
106
|
+
export interface HashlineApplyOptions {
|
|
107
|
+
autoDropPureInsertDuplicates?: boolean;
|
|
108
|
+
}
|
|
106
109
|
|
|
107
110
|
export interface SplitHashlineOptions {
|
|
108
111
|
cwd?: string;
|
|
@@ -140,7 +143,7 @@ const HL_OUTPUT_PREFIX_SEPARATOR_RE = `[:${HL_BODY_SEP_RE_RAW}]`;
|
|
|
140
143
|
const HL_PREFIX_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*(?:[+*]\\s*)?\\d+[a-z]{2}${HL_OUTPUT_PREFIX_SEPARATOR_RE}`);
|
|
141
144
|
const HL_PREFIX_PLUS_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*\\+\\s*\\d+[a-z]{2}${HL_OUTPUT_PREFIX_SEPARATOR_RE}`);
|
|
142
145
|
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
143
|
-
const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\
|
|
146
|
+
const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bUse :L?\d+/;
|
|
144
147
|
|
|
145
148
|
const HL_HASH_HINT_RE = /^[a-z]{2}$/i;
|
|
146
149
|
const HL_ANCHOR_EXAMPLES = describeAnchorExamples("160");
|
|
@@ -1003,10 +1006,150 @@ function deleteEditForAutoAbsorbedLine(
|
|
|
1003
1006
|
};
|
|
1004
1007
|
}
|
|
1005
1008
|
|
|
1009
|
+
interface HashlinePureInsertGroup {
|
|
1010
|
+
startIndex: number;
|
|
1011
|
+
endIndex: number;
|
|
1012
|
+
sourceLineNum: number;
|
|
1013
|
+
cursor: HashlineCursor;
|
|
1014
|
+
payload: string[];
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function cursorMatches(a: HashlineCursor, b: HashlineCursor): boolean {
|
|
1018
|
+
if (a.kind !== b.kind) return false;
|
|
1019
|
+
if (a.kind === "bof" || a.kind === "eof") return true;
|
|
1020
|
+
const aAnchor = (a as { anchor: Anchor }).anchor;
|
|
1021
|
+
const bAnchor = (b as { anchor: Anchor }).anchor;
|
|
1022
|
+
return aAnchor.line === bAnchor.line && aAnchor.hash === bAnchor.hash;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Collects a run of consecutive `insert` edits that all share the same
|
|
1027
|
+
* `lineNum` and `cursor`, IFF that run is not immediately followed by a
|
|
1028
|
+
* `delete` at the same `lineNum` (which would make it a replacement group
|
|
1029
|
+
* instead). Returns the contiguous payload so we can check it for boundary
|
|
1030
|
+
* duplicates against the file.
|
|
1031
|
+
*/
|
|
1032
|
+
function findPureInsertGroup(edits: HashlineEdit[], startIndex: number): HashlinePureInsertGroup | undefined {
|
|
1033
|
+
const first = edits[startIndex];
|
|
1034
|
+
if (first?.kind !== "insert") return undefined;
|
|
1035
|
+
|
|
1036
|
+
const sourceLineNum = first.lineNum;
|
|
1037
|
+
const cursor = first.cursor;
|
|
1038
|
+
const payload: string[] = [];
|
|
1039
|
+
let index = startIndex;
|
|
1040
|
+
while (index < edits.length) {
|
|
1041
|
+
const edit = edits[index];
|
|
1042
|
+
if (edit.kind !== "insert" || edit.lineNum !== sourceLineNum) break;
|
|
1043
|
+
if (!cursorMatches(edit.cursor, cursor)) break;
|
|
1044
|
+
payload.push(edit.text);
|
|
1045
|
+
index++;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// If the run is followed by a delete at the same source lineNum, this is a
|
|
1049
|
+
// replacement group (handled by absorbReplacement…). Decline.
|
|
1050
|
+
if (index < edits.length && edits[index].kind === "delete" && edits[index].lineNum === sourceLineNum) {
|
|
1051
|
+
return undefined;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return { startIndex, endIndex: index - 1, sourceLineNum, cursor, payload };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* For a pure-insert group, locate the file region adjacent to the insertion
|
|
1059
|
+
* point. Returns 0-indexed bounds:
|
|
1060
|
+
* - `aboveEndIdx`: index of the last file line strictly above the insertion
|
|
1061
|
+
* point (-1 if none).
|
|
1062
|
+
* - `belowStartIdx`: index of the first file line strictly below the
|
|
1063
|
+
* insertion point (`fileLines.length` if none).
|
|
1064
|
+
*/
|
|
1065
|
+
function pureInsertNeighborhood(
|
|
1066
|
+
cursor: HashlineCursor,
|
|
1067
|
+
fileLines: string[],
|
|
1068
|
+
): { aboveEndIdx: number; belowStartIdx: number } {
|
|
1069
|
+
if (cursor.kind === "bof") return { aboveEndIdx: -1, belowStartIdx: 0 };
|
|
1070
|
+
if (cursor.kind === "eof") return { aboveEndIdx: fileLines.length - 1, belowStartIdx: fileLines.length };
|
|
1071
|
+
if (cursor.kind === "before_anchor") {
|
|
1072
|
+
return { aboveEndIdx: cursor.anchor.line - 2, belowStartIdx: cursor.anchor.line - 1 };
|
|
1073
|
+
}
|
|
1074
|
+
// after_anchor
|
|
1075
|
+
return { aboveEndIdx: cursor.anchor.line - 1, belowStartIdx: cursor.anchor.line };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
interface PureInsertAbsorbResult {
|
|
1079
|
+
keptPayload: string[];
|
|
1080
|
+
absorbedLeading: number;
|
|
1081
|
+
absorbedTrailing: number;
|
|
1082
|
+
leadingFileRange?: { start: number; end: number }; // 1-indexed inclusive
|
|
1083
|
+
trailingFileRange?: { start: number; end: number }; // 1-indexed inclusive
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Mirror of replacement-absorb's prefix/suffix block check, but for pure
|
|
1088
|
+
* inserts: drop payload lines that exactly duplicate the file lines
|
|
1089
|
+
* immediately above (leading) or immediately below (trailing) the insertion
|
|
1090
|
+
* point. Requires a minimum run of 2 to avoid spurious single-line matches,
|
|
1091
|
+
* matching the existing replacement-absorb threshold.
|
|
1092
|
+
*/
|
|
1093
|
+
function tryAbsorbPureInsertGroup(group: HashlinePureInsertGroup, fileLines: string[]): PureInsertAbsorbResult {
|
|
1094
|
+
const empty: PureInsertAbsorbResult = { keptPayload: group.payload, absorbedLeading: 0, absorbedTrailing: 0 };
|
|
1095
|
+
if (group.payload.length < 2) return empty;
|
|
1096
|
+
|
|
1097
|
+
const { aboveEndIdx, belowStartIdx } = pureInsertNeighborhood(group.cursor, fileLines);
|
|
1098
|
+
|
|
1099
|
+
// Leading: payload[0..k-1] vs fileLines[aboveEndIdx-k+1 .. aboveEndIdx].
|
|
1100
|
+
let absorbedLeading = 0;
|
|
1101
|
+
const maxLead = Math.min(group.payload.length, aboveEndIdx + 1);
|
|
1102
|
+
for (let count = maxLead; count >= 2; count--) {
|
|
1103
|
+
let ok = true;
|
|
1104
|
+
for (let offset = 0; offset < count; offset++) {
|
|
1105
|
+
if (group.payload[offset] !== fileLines[aboveEndIdx - count + 1 + offset]) {
|
|
1106
|
+
ok = false;
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (ok) {
|
|
1111
|
+
absorbedLeading = count;
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Trailing: payload[len-k..len-1] vs fileLines[belowStartIdx..belowStartIdx+k-1].
|
|
1117
|
+
// Don't double-count payload lines already absorbed as leading.
|
|
1118
|
+
let absorbedTrailing = 0;
|
|
1119
|
+
const remaining = group.payload.length - absorbedLeading;
|
|
1120
|
+
const maxTrail = Math.min(remaining, fileLines.length - belowStartIdx);
|
|
1121
|
+
for (let count = maxTrail; count >= 2; count--) {
|
|
1122
|
+
let ok = true;
|
|
1123
|
+
for (let offset = 0; offset < count; offset++) {
|
|
1124
|
+
if (group.payload[group.payload.length - count + offset] !== fileLines[belowStartIdx + offset]) {
|
|
1125
|
+
ok = false;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (ok) {
|
|
1130
|
+
absorbedTrailing = count;
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (absorbedLeading === 0 && absorbedTrailing === 0) return empty;
|
|
1136
|
+
|
|
1137
|
+
return {
|
|
1138
|
+
keptPayload: group.payload.slice(absorbedLeading, group.payload.length - absorbedTrailing),
|
|
1139
|
+
absorbedLeading,
|
|
1140
|
+
absorbedTrailing,
|
|
1141
|
+
leadingFileRange:
|
|
1142
|
+
absorbedLeading > 0 ? { start: aboveEndIdx - absorbedLeading + 2, end: aboveEndIdx + 1 } : undefined,
|
|
1143
|
+
trailingFileRange:
|
|
1144
|
+
absorbedTrailing > 0 ? { start: belowStartIdx + 1, end: belowStartIdx + absorbedTrailing } : undefined,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1006
1148
|
function absorbReplacementBoundaryDuplicates(
|
|
1007
1149
|
edits: HashlineEdit[],
|
|
1008
1150
|
fileLines: string[],
|
|
1009
1151
|
warnings: string[],
|
|
1152
|
+
options: HashlineApplyOptions,
|
|
1010
1153
|
): HashlineEdit[] {
|
|
1011
1154
|
let nextSyntheticIndex = edits.length;
|
|
1012
1155
|
const absorbed: HashlineEdit[] = [];
|
|
@@ -1021,6 +1164,47 @@ function absorbReplacementBoundaryDuplicates(
|
|
|
1021
1164
|
for (let index = 0; index < edits.length; index++) {
|
|
1022
1165
|
const group = findReplacementGroup(edits, index);
|
|
1023
1166
|
if (!group) {
|
|
1167
|
+
if (options.autoDropPureInsertDuplicates) {
|
|
1168
|
+
const pureInsert = findPureInsertGroup(edits, index);
|
|
1169
|
+
if (pureInsert) {
|
|
1170
|
+
const result = tryAbsorbPureInsertGroup(pureInsert, fileLines);
|
|
1171
|
+
if (result.absorbedLeading > 0 || result.absorbedTrailing > 0) {
|
|
1172
|
+
if (result.leadingFileRange) {
|
|
1173
|
+
const { start, end } = result.leadingFileRange;
|
|
1174
|
+
const key = `pure-insert-leading:${start}..${end}`;
|
|
1175
|
+
if (!emittedAbsorbKeys.has(key)) {
|
|
1176
|
+
emittedAbsorbKeys.add(key);
|
|
1177
|
+
warnings.push(
|
|
1178
|
+
`Auto-dropped ${result.absorbedLeading} duplicate line(s) at the start of insert at line ${pureInsert.sourceLineNum} ` +
|
|
1179
|
+
`(file lines ${start}..${end} already match the payload's leading lines).`,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (result.trailingFileRange) {
|
|
1184
|
+
const { start, end } = result.trailingFileRange;
|
|
1185
|
+
const key = `pure-insert-trailing:${start}..${end}`;
|
|
1186
|
+
if (!emittedAbsorbKeys.has(key)) {
|
|
1187
|
+
emittedAbsorbKeys.add(key);
|
|
1188
|
+
warnings.push(
|
|
1189
|
+
`Auto-dropped ${result.absorbedTrailing} duplicate line(s) at the end of insert at line ${pureInsert.sourceLineNum} ` +
|
|
1190
|
+
`(file lines ${start}..${end} already match the payload's trailing lines).`,
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
for (const text of result.keptPayload) {
|
|
1195
|
+
absorbed.push({
|
|
1196
|
+
kind: "insert",
|
|
1197
|
+
cursor: cloneCursor(pureInsert.cursor),
|
|
1198
|
+
text,
|
|
1199
|
+
lineNum: pureInsert.sourceLineNum,
|
|
1200
|
+
index: nextSyntheticIndex++,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
index = pureInsert.endIndex;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1024
1208
|
absorbed.push(edits[index]);
|
|
1025
1209
|
continue;
|
|
1026
1210
|
}
|
|
@@ -1094,7 +1278,11 @@ function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map<number, IndexedEdit[
|
|
|
1094
1278
|
return byLine;
|
|
1095
1279
|
}
|
|
1096
1280
|
|
|
1097
|
-
export function applyHashlineEdits(
|
|
1281
|
+
export function applyHashlineEdits(
|
|
1282
|
+
text: string,
|
|
1283
|
+
edits: HashlineEdit[],
|
|
1284
|
+
options: HashlineApplyOptions = {},
|
|
1285
|
+
): HashlineApplyResult {
|
|
1098
1286
|
if (edits.length === 0) return { lines: text, firstChangedLine: undefined };
|
|
1099
1287
|
|
|
1100
1288
|
const fileLines = text.split("\n");
|
|
@@ -1109,7 +1297,7 @@ export function applyHashlineEdits(text: string, edits: HashlineEdit[]): Hashlin
|
|
|
1109
1297
|
const mismatches = validateHashlineAnchors(edits, fileLines, warnings);
|
|
1110
1298
|
if (mismatches.length > 0) throw new HashlineMismatchError(mismatches, fileLines);
|
|
1111
1299
|
|
|
1112
|
-
const normalizedEdits = absorbReplacementBoundaryDuplicates(edits, fileLines, warnings);
|
|
1300
|
+
const normalizedEdits = absorbReplacementBoundaryDuplicates(edits, fileLines, warnings, options);
|
|
1113
1301
|
|
|
1114
1302
|
// Normalize after_anchor inserts to before_anchor of the next line, or EOF
|
|
1115
1303
|
// when the anchor is the final line. This keeps the bucketing logic below
|
|
@@ -1332,6 +1520,7 @@ async function readHashlineFileText(file: { text(): Promise<string> }, pathText:
|
|
|
1332
1520
|
export async function computeHashlineDiff(
|
|
1333
1521
|
input: { input: string; path?: string },
|
|
1334
1522
|
cwd: string,
|
|
1523
|
+
options: HashlineApplyOptions = {},
|
|
1335
1524
|
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
1336
1525
|
try {
|
|
1337
1526
|
const sections = splitHashlineInputs(input.input, { cwd, path: input.path });
|
|
@@ -1344,7 +1533,7 @@ export async function computeHashlineDiff(
|
|
|
1344
1533
|
const rawContent = await readHashlineFileText(Bun.file(absolutePath), section.path);
|
|
1345
1534
|
const { text: content } = stripBom(rawContent);
|
|
1346
1535
|
const normalized = normalizeToLF(content);
|
|
1347
|
-
const result = applyHashlineEdits(normalized, parseHashline(section.diff));
|
|
1536
|
+
const result = applyHashlineEdits(normalized, parseHashline(section.diff), options);
|
|
1348
1537
|
if (normalized === result.lines) return { error: `No changes would be made to ${section.path}.` };
|
|
1349
1538
|
return generateDiffString(normalized, result.lines);
|
|
1350
1539
|
} catch (err) {
|
|
@@ -1382,6 +1571,12 @@ function formatNoChangeDiagnostic(pathText: string): string {
|
|
|
1382
1571
|
return `Edits to ${pathText} resulted in no changes being made.`;
|
|
1383
1572
|
}
|
|
1384
1573
|
|
|
1574
|
+
function getHashlineApplyOptions(session: ToolSession): HashlineApplyOptions {
|
|
1575
|
+
return {
|
|
1576
|
+
autoDropPureInsertDuplicates: session.settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1385
1580
|
function getTextContent(result: AgentToolResult<EditToolDetails>): string {
|
|
1386
1581
|
return result.content.map(part => (part.type === "text" ? part.text : "")).join("\n");
|
|
1387
1582
|
}
|
|
@@ -1408,7 +1603,7 @@ async function preflightHashlineSection(options: ExecuteHashlineSingleOptions &
|
|
|
1408
1603
|
|
|
1409
1604
|
const { text } = stripBom(source.rawContent);
|
|
1410
1605
|
const normalized = normalizeToLF(text);
|
|
1411
|
-
const result = applyHashlineEdits(normalized, edits);
|
|
1606
|
+
const result = applyHashlineEdits(normalized, edits, getHashlineApplyOptions(session));
|
|
1412
1607
|
if (normalized === result.lines) throw new Error(formatNoChangeDiagnostic(sectionPath));
|
|
1413
1608
|
}
|
|
1414
1609
|
|
|
@@ -1436,7 +1631,7 @@ async function executeHashlineSection(
|
|
|
1436
1631
|
const { bom, text } = stripBom(source.rawContent);
|
|
1437
1632
|
const originalEnding = detectLineEnding(text);
|
|
1438
1633
|
const originalNormalized = normalizeToLF(text);
|
|
1439
|
-
const result = applyHashlineEdits(originalNormalized, edits);
|
|
1634
|
+
const result = applyHashlineEdits(originalNormalized, edits, getHashlineApplyOptions(session));
|
|
1440
1635
|
|
|
1441
1636
|
if (originalNormalized === result.lines) {
|
|
1442
1637
|
return {
|
package/src/edit/streaming.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface StreamingDiffContext {
|
|
|
32
32
|
signal: AbortSignal;
|
|
33
33
|
fuzzyThreshold?: number;
|
|
34
34
|
allowFuzzy?: boolean;
|
|
35
|
+
hashlineAutoDropPureInsertDuplicates?: boolean;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface EditStreamingStrategy<Args = unknown> {
|
|
@@ -222,7 +223,9 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
222
223
|
async computeDiffPreview(args, ctx) {
|
|
223
224
|
if (typeof args.input !== "string" || args.input.length === 0) return null;
|
|
224
225
|
ctx.signal.throwIfAborted();
|
|
225
|
-
const result = await computeHashlineDiff({ input: args.input, path: args.path }, ctx.cwd
|
|
226
|
+
const result = await computeHashlineDiff({ input: args.input, path: args.path }, ctx.cwd, {
|
|
227
|
+
autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
|
|
228
|
+
});
|
|
226
229
|
ctx.signal.throwIfAborted();
|
|
227
230
|
if ("error" in result && !args.path) return [{ path: "", error: result.error }];
|
|
228
231
|
return [toPerFilePreview(args.path ?? "", result)];
|
package/src/export/html/index.ts
CHANGED
|
@@ -124,7 +124,7 @@ export async function exportSessionToHtml(
|
|
|
124
124
|
header: sm.getHeader(),
|
|
125
125
|
entries: sm.getEntries(),
|
|
126
126
|
leafId: sm.getLeafId(),
|
|
127
|
-
systemPrompt: state?.systemPrompt,
|
|
127
|
+
systemPrompt: state?.systemPrompt.join("\n\n"),
|
|
128
128
|
tools: state?.tools?.map(t => ({ name: t.name, description: t.description })),
|
|
129
129
|
};
|
|
130
130
|
|
|
@@ -55,7 +55,7 @@ import type {
|
|
|
55
55
|
/** Combined result from all before_agent_start handlers */
|
|
56
56
|
interface BeforeAgentStartCombinedResult {
|
|
57
57
|
messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
|
|
58
|
-
systemPrompt?: string;
|
|
58
|
+
systemPrompt?: string[];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export type ExtensionErrorListener = (error: ExtensionError) => void;
|
|
@@ -168,7 +168,7 @@ export class ExtensionRunner {
|
|
|
168
168
|
#hasPendingMessagesFn: () => boolean = () => false;
|
|
169
169
|
#getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
|
170
170
|
#compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
|
|
171
|
-
#getSystemPromptFn: () => string = () =>
|
|
171
|
+
#getSystemPromptFn: () => string[] = () => [];
|
|
172
172
|
#newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
|
173
173
|
#branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
174
174
|
#navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
@@ -795,7 +795,7 @@ export class ExtensionRunner {
|
|
|
795
795
|
async emitBeforeAgentStart(
|
|
796
796
|
prompt: string,
|
|
797
797
|
images: ImageContent[] | undefined,
|
|
798
|
-
systemPrompt: string,
|
|
798
|
+
systemPrompt: string[],
|
|
799
799
|
): Promise<BeforeAgentStartCombinedResult | undefined> {
|
|
800
800
|
const ctx = this.createContext();
|
|
801
801
|
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
|
|
@@ -240,7 +240,7 @@ export interface ExtensionContext {
|
|
|
240
240
|
/** Gracefully shutdown and exit. */
|
|
241
241
|
shutdown(): void;
|
|
242
242
|
/** Get the current effective system prompt. */
|
|
243
|
-
getSystemPrompt(): string;
|
|
243
|
+
getSystemPrompt(): string[];
|
|
244
244
|
/** @deprecated Use hasPendingMessages() instead */
|
|
245
245
|
hasQueuedMessages(): boolean;
|
|
246
246
|
}
|
|
@@ -492,7 +492,7 @@ export interface BeforeAgentStartEvent {
|
|
|
492
492
|
type: "before_agent_start";
|
|
493
493
|
prompt: string;
|
|
494
494
|
images?: ImageContent[];
|
|
495
|
-
systemPrompt: string;
|
|
495
|
+
systemPrompt: string[];
|
|
496
496
|
}
|
|
497
497
|
|
|
498
498
|
/** Fired when an agent loop starts */
|
|
@@ -876,7 +876,7 @@ export interface ToolResultEventResult {
|
|
|
876
876
|
export interface BeforeAgentStartEventResult {
|
|
877
877
|
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details" | "attribution">;
|
|
878
878
|
/** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */
|
|
879
|
-
systemPrompt?: string;
|
|
879
|
+
systemPrompt?: string[];
|
|
880
880
|
}
|
|
881
881
|
|
|
882
882
|
export interface SessionBeforeSwitchResult {
|
|
@@ -1318,7 +1318,7 @@ export interface ExtensionContextActions {
|
|
|
1318
1318
|
shutdown: () => void;
|
|
1319
1319
|
getContextUsage: () => ContextUsage | undefined;
|
|
1320
1320
|
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
1321
|
-
getSystemPrompt: () => string;
|
|
1321
|
+
getSystemPrompt: () => string[];
|
|
1322
1322
|
}
|
|
1323
1323
|
|
|
1324
1324
|
/** Actions for ExtensionCommandContext (ctx.* in command handlers). */
|
package/src/main.ts
CHANGED
|
@@ -540,11 +540,11 @@ async function buildSessionOptions(
|
|
|
540
540
|
|
|
541
541
|
// System prompt
|
|
542
542
|
if (resolvedSystemPrompt && resolvedAppendPrompt) {
|
|
543
|
-
options.systemPrompt =
|
|
543
|
+
options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, resolvedAppendPrompt, ...defaultPrompt.slice(1)];
|
|
544
544
|
} else if (resolvedSystemPrompt) {
|
|
545
|
-
options.systemPrompt = resolvedSystemPrompt;
|
|
545
|
+
options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, ...defaultPrompt.slice(1)];
|
|
546
546
|
} else if (resolvedAppendPrompt) {
|
|
547
|
-
options.systemPrompt = defaultPrompt =>
|
|
547
|
+
options.systemPrompt = defaultPrompt => [...defaultPrompt, resolvedAppendPrompt];
|
|
548
548
|
}
|
|
549
549
|
|
|
550
550
|
// Tools
|
package/src/memories/index.ts
CHANGED
|
@@ -602,7 +602,7 @@ async function runStage1Job(options: {
|
|
|
602
602
|
const response = await completeSimple(
|
|
603
603
|
model,
|
|
604
604
|
{
|
|
605
|
-
systemPrompt: stageOneSystemTemplate,
|
|
605
|
+
systemPrompt: [stageOneSystemTemplate],
|
|
606
606
|
messages: [{ role: "user", content: [{ type: "text", text: inputPrompt }], timestamp: Date.now() }],
|
|
607
607
|
},
|
|
608
608
|
{
|
|
@@ -8,6 +8,7 @@ import type { ToolExecutionHandle } from "./tool-execution";
|
|
|
8
8
|
type ReadRenderArgs = {
|
|
9
9
|
path?: string;
|
|
10
10
|
file_path?: string;
|
|
11
|
+
// Legacy field from the old schema; tolerated for rebuilt transcripts.
|
|
11
12
|
sel?: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -37,7 +38,6 @@ function getSuffixResolution(details: ReadToolResultDetails | undefined): ReadTo
|
|
|
37
38
|
type ReadEntry = {
|
|
38
39
|
toolCallId: string;
|
|
39
40
|
path: string;
|
|
40
|
-
sel?: string;
|
|
41
41
|
status: "pending" | "success" | "warning" | "error";
|
|
42
42
|
correctedFrom?: string;
|
|
43
43
|
contentText?: string;
|
|
@@ -62,15 +62,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
62
62
|
|
|
63
63
|
updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
|
|
64
64
|
if (!toolCallId) return;
|
|
65
|
-
const
|
|
65
|
+
const basePath = args.file_path || args.path || "";
|
|
66
|
+
const rawPath = args.sel ? `${basePath}:${args.sel}` : basePath;
|
|
66
67
|
const entry: ReadEntry = this.#entries.get(toolCallId) ?? {
|
|
67
68
|
toolCallId,
|
|
68
69
|
path: rawPath,
|
|
69
|
-
sel: args.sel,
|
|
70
70
|
status: "pending",
|
|
71
71
|
};
|
|
72
72
|
entry.path = rawPath;
|
|
73
|
-
entry.sel = args.sel;
|
|
74
73
|
this.#entries.set(toolCallId, entry);
|
|
75
74
|
this.#updateDisplay();
|
|
76
75
|
}
|
|
@@ -172,9 +171,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
172
171
|
#addContentPreview(entry: ReadEntry): void {
|
|
173
172
|
const lang = getLanguageFromPath(entry.path);
|
|
174
173
|
const filePath = shortenPath(entry.path);
|
|
175
|
-
const selectionSuffix = entry.sel ? `:${entry.sel}` : "";
|
|
176
174
|
const correctionSuffix = entry.correctedFrom ? ` (corrected from ${shortenPath(entry.correctedFrom)})` : "";
|
|
177
|
-
const title = filePath ? `Read ${filePath}${
|
|
175
|
+
const title = filePath ? `Read ${filePath}${correctionSuffix}` : "Read";
|
|
178
176
|
let cachedWidth: number | undefined;
|
|
179
177
|
let cachedLines: string[] | undefined;
|
|
180
178
|
const expanded = this.#expanded;
|
|
@@ -211,9 +209,6 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
211
209
|
#formatPath(entry: ReadEntry): string {
|
|
212
210
|
const filePath = shortenPath(entry.path);
|
|
213
211
|
let pathDisplay = filePath ? theme.fg("accent", filePath) : theme.fg("toolOutput", "…");
|
|
214
|
-
if (entry.sel) {
|
|
215
|
-
pathDisplay += theme.fg("warning", `:${entry.sel}`);
|
|
216
|
-
}
|
|
217
212
|
if (entry.correctedFrom) {
|
|
218
213
|
pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(entry.correctedFrom)})`);
|
|
219
214
|
}
|
|
@@ -67,6 +67,7 @@ export interface ToolExecutionOptions {
|
|
|
67
67
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
68
68
|
editFuzzyThreshold?: number;
|
|
69
69
|
editAllowFuzzy?: boolean;
|
|
70
|
+
hashlineAutoDropPureInsertDuplicates?: boolean;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export interface ToolExecutionHandle {
|
|
@@ -100,6 +101,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
100
101
|
#showImages: boolean;
|
|
101
102
|
#editFuzzyThreshold: number | undefined;
|
|
102
103
|
#editAllowFuzzy: boolean | undefined;
|
|
104
|
+
#hashlineAutoDropPureInsertDuplicates: boolean | undefined;
|
|
103
105
|
#isPartial = true;
|
|
104
106
|
#tool?: AgentTool;
|
|
105
107
|
#ui: TUI;
|
|
@@ -147,6 +149,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
147
149
|
this.#showImages = options.showImages ?? true;
|
|
148
150
|
this.#editFuzzyThreshold = options.editFuzzyThreshold;
|
|
149
151
|
this.#editAllowFuzzy = options.editAllowFuzzy;
|
|
152
|
+
this.#hashlineAutoDropPureInsertDuplicates = options.hashlineAutoDropPureInsertDuplicates;
|
|
150
153
|
this.#tool = tool;
|
|
151
154
|
this.#ui = ui;
|
|
152
155
|
this.#cwd = cwd;
|
|
@@ -248,6 +251,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
248
251
|
signal: controller.signal,
|
|
249
252
|
fuzzyThreshold: this.#editFuzzyThreshold,
|
|
250
253
|
allowFuzzy: this.#editAllowFuzzy,
|
|
254
|
+
hashlineAutoDropPureInsertDuplicates: this.#hashlineAutoDropPureInsertDuplicates,
|
|
251
255
|
});
|
|
252
256
|
if (controller.signal.aborted) return;
|
|
253
257
|
if (previews) {
|
|
@@ -280,6 +280,7 @@ export class EventController {
|
|
|
280
280
|
showImages: settings.get("terminal.showImages"),
|
|
281
281
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
282
282
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
283
|
+
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
283
284
|
},
|
|
284
285
|
tool,
|
|
285
286
|
this.ctx.ui,
|
|
@@ -383,6 +384,7 @@ export class EventController {
|
|
|
383
384
|
showImages: settings.get("terminal.showImages"),
|
|
384
385
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
385
386
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
387
|
+
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
386
388
|
},
|
|
387
389
|
tool,
|
|
388
390
|
this.ctx.ui,
|
|
@@ -89,7 +89,7 @@ export interface RpcSessionState {
|
|
|
89
89
|
queuedMessageCount: number;
|
|
90
90
|
todoPhases: TodoPhase[];
|
|
91
91
|
/** For session dump / export (plain-text parity with /dump). */
|
|
92
|
-
systemPrompt?: string;
|
|
92
|
+
systemPrompt?: string[];
|
|
93
93
|
dumpTools?: Array<{ name: string; description: string; parameters: unknown }>;
|
|
94
94
|
/** Current context window usage. Null tokens/percent when unknown (e.g. right after compaction). */
|
|
95
95
|
contextUsage?: ContextUsage;
|
|
@@ -18,13 +18,13 @@ const CELL_FILLED_MESSAGES = "⛃";
|
|
|
18
18
|
const CELL_FREE = "⛶";
|
|
19
19
|
const CELL_BUFFER = "⛝";
|
|
20
20
|
|
|
21
|
-
type CategoryId = "systemPrompt" | "systemTools" | "skills" | "messages";
|
|
21
|
+
type CategoryId = "systemPrompt" | "systemContext" | "systemTools" | "skills" | "messages";
|
|
22
22
|
|
|
23
23
|
interface CategoryInfo {
|
|
24
24
|
id: CategoryId;
|
|
25
25
|
label: string;
|
|
26
26
|
tokens: number;
|
|
27
|
-
color: "accent" | "warning" | "success" | "userMessageText";
|
|
27
|
+
color: "accent" | "warning" | "success" | "userMessageText" | "customMessageLabel";
|
|
28
28
|
glyph: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -86,12 +86,19 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
|
|
|
86
86
|
// Tools = JSON tool schema sent separately on the wire
|
|
87
87
|
// Skills = the skill list embedded in the system prompt
|
|
88
88
|
// Messages = conversation messages
|
|
89
|
-
const
|
|
90
|
-
const
|
|
89
|
+
const systemPromptTokens = Math.max(0, countTokens(session.systemPrompt[0] ?? "") - skillsTokens);
|
|
90
|
+
const systemContextTokens = countTokens(session.systemPrompt.slice(1));
|
|
91
91
|
|
|
92
92
|
const categories: CategoryInfo[] = [
|
|
93
93
|
{ id: "systemPrompt", label: "System prompt", tokens: systemPromptTokens, color: "accent", glyph: CELL_FILLED },
|
|
94
94
|
{ id: "systemTools", label: "System tools", tokens: toolsTokens, color: "warning", glyph: CELL_FILLED },
|
|
95
|
+
{
|
|
96
|
+
id: "systemContext",
|
|
97
|
+
label: "System context",
|
|
98
|
+
tokens: systemContextTokens,
|
|
99
|
+
color: "customMessageLabel",
|
|
100
|
+
glyph: CELL_FILLED,
|
|
101
|
+
},
|
|
95
102
|
{ id: "skills", label: "Skills", tokens: skillsTokens, color: "success", glyph: CELL_FILLED },
|
|
96
103
|
{
|
|
97
104
|
id: "messages",
|
|
@@ -134,7 +141,7 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
|
|
|
134
141
|
|
|
135
142
|
interface CellSpec {
|
|
136
143
|
glyph: string;
|
|
137
|
-
color: "accent" | "warning" | "success" | "userMessageText" | "muted" | "dim";
|
|
144
|
+
color: "accent" | "warning" | "success" | "userMessageText" | "customMessageLabel" | "muted" | "dim";
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
function planCells(breakdown: ContextBreakdown): CellSpec[] {
|
|
@@ -336,6 +336,7 @@ export class UiHelpers {
|
|
|
336
336
|
showImages: settings.get("terminal.showImages"),
|
|
337
337
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
338
338
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
339
|
+
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
339
340
|
},
|
|
340
341
|
tool,
|
|
341
342
|
this.ctx.ui,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<workstation>
|
|
2
|
+
{{#list environment prefix="- " join="\n"}}{{label}}: {{value}}{{/list}}
|
|
3
|
+
</workstation>
|
|
4
|
+
|
|
5
|
+
{{#if contextFiles.length}}
|
|
6
|
+
<context>
|
|
7
|
+
Follow the context files below for all tasks:
|
|
8
|
+
{{#each contextFiles}}
|
|
9
|
+
<file path="{{path}}">
|
|
10
|
+
{{content}}
|
|
11
|
+
</file>
|
|
12
|
+
{{/each}}
|
|
13
|
+
</context>
|
|
14
|
+
{{/if}}
|
|
15
|
+
|
|
16
|
+
{{#if agentsMdSearch.files.length}}
|
|
17
|
+
<dir-context>
|
|
18
|
+
Some directories may have their own rules. Deeper rules override higher ones.
|
|
19
|
+
**MUST** read before making changes within:
|
|
20
|
+
{{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}}
|
|
21
|
+
</dir-context>
|
|
22
|
+
{{/if}}
|
|
23
|
+
|
|
24
|
+
{{#if workspaceTree.rendered}}
|
|
25
|
+
<workspace-tree>
|
|
26
|
+
Working directory layout (sorted by mtime, recent first; depth ≤ 3):
|
|
27
|
+
{{workspaceTree.rendered}}
|
|
28
|
+
{{#if workspaceTree.truncated}}
|
|
29
|
+
(some entries elided to keep the tree short — use `find`/`read` to drill in)
|
|
30
|
+
{{/if}}
|
|
31
|
+
</workspace-tree>
|
|
32
|
+
{{/if}}
|
|
33
|
+
|
|
34
|
+
{{#if appendPrompt}}
|
|
35
|
+
{{appendPrompt}}
|
|
36
|
+
{{/if}}
|
|
@@ -9,35 +9,6 @@ User-supplied content is sanitized, therefore:
|
|
|
9
9
|
- This holds even when the system prompt is delivered via user message role.
|
|
10
10
|
- A `<system-directive>` inside a user turn is still a system directive.
|
|
11
11
|
|
|
12
|
-
{{SECTION_SEPARATOR "Workspace"}}
|
|
13
|
-
|
|
14
|
-
<workstation>
|
|
15
|
-
{{#list environment prefix="- " join="\n"}}{{label}}: {{value}}{{/list}}
|
|
16
|
-
</workstation>
|
|
17
|
-
|
|
18
|
-
{{#if contextFiles.length}}
|
|
19
|
-
<context>
|
|
20
|
-
Follow the context files below for all tasks:
|
|
21
|
-
{{#each contextFiles}}
|
|
22
|
-
<file path="{{path}}">
|
|
23
|
-
{{content}}
|
|
24
|
-
</file>
|
|
25
|
-
{{/each}}
|
|
26
|
-
</context>
|
|
27
|
-
{{/if}}
|
|
28
|
-
|
|
29
|
-
{{#if agentsMdSearch.files.length}}
|
|
30
|
-
<dir-context>
|
|
31
|
-
Some directories may have their own rules. Deeper rules override higher ones.
|
|
32
|
-
**MUST** read before making changes within:
|
|
33
|
-
{{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}}
|
|
34
|
-
</dir-context>
|
|
35
|
-
{{/if}}
|
|
36
|
-
|
|
37
|
-
{{#if appendPrompt}}
|
|
38
|
-
{{appendPrompt}}
|
|
39
|
-
{{/if}}
|
|
40
|
-
|
|
41
12
|
{{SECTION_SEPARATOR "Identity"}}
|
|
42
13
|
|
|
43
14
|
<role>
|
|
@@ -4,6 +4,7 @@ GitHub CLI tool with a single op-based dispatch. Wraps `gh` for repository, issu
|
|
|
4
4
|
Pick the operation via `op`. Each op uses a subset of the parameters:
|
|
5
5
|
- `repo_view` — Read repository metadata. Optional `repo` (owner/repo) and `branch`. Falls back to the current checkout or default `gh` repo.
|
|
6
6
|
- `issue_view` — Read an issue. Required `issue` (number or URL). Optional `repo`. Set `comments: false` to skip discussion.
|
|
7
|
+
- `pr_create` — Create a pull request. Either provide `title` (and optional `body`) or set `fill: true` to auto-fill from commits. Optional `base` (target, defaults to repo default), `head` (source, defaults to current branch), `draft`, `repo`, `reviewer[]`, `assignee[]`, `label[]`. Returns the new PR URL plus a summary.
|
|
7
8
|
- `pr_view` — Read one or more pull requests, including reviews and inline review comments. Optional `pr` (number, URL, branch, or array of any — pass an array to fetch multiple PRs in one call); omitting it targets the current branch's PR. Optional `repo`. Set `comments: false` for a lighter summary.
|
|
8
9
|
- `pr_diff` — Read one or more pull request diffs. Optional `pr` (single identifier or array for batch). Optional `repo`. Set `nameOnly: true` for changed file names. Use `exclude` to drop generated paths from the diff.
|
|
9
10
|
- `pr_checkout` — Check one or more pull requests out into dedicated git worktrees. Optional `pr` (number, URL, branch, or array of any of those — pass an array to batch-check-out multiple PRs in one call), `repo`, `force` (reset existing local branch).
|