@oh-my-pi/pi-coding-agent 12.19.0 → 12.19.3

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 CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.19.3] - 2026-02-22
6
+ ### Added
7
+
8
+ - Added `pty` parameter to bash tool to enable PTY mode for commands requiring a real terminal (e.g., sudo, ssh, top, less)
9
+
10
+ ### Changed
11
+
12
+ - Changed bash tool to use per-command PTY control instead of global virtual terminal setting
13
+
14
+ ### Removed
15
+
16
+ - Removed `bash.virtualTerminal` setting; use the `pty` parameter on individual bash commands instead
17
+
18
+ ## [12.19.1] - 2026-02-22
19
+ ### Removed
20
+
21
+ - Removed `replaceText` edit operation from hashline mode (substring-based text replacement)
22
+ - Removed autocorrect heuristics that attempted to detect and fix line merges and formatting rewrites in hashline edits
23
+
5
24
  ## [12.19.0] - 2026-02-22
6
25
  ### Added
7
26
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "12.19.0",
4
+ "version": "12.19.3",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
- "author": "Can Bölük",
7
+ "author": "Can Boluk",
8
8
  "contributors": [
9
9
  "Mario Zechner"
10
10
  ],
@@ -40,33 +40,33 @@
40
40
  "test": "bun test"
41
41
  },
42
42
  "dependencies": {
43
- "@mozilla/readability": "0.6.0",
44
- "@oh-my-pi/omp-stats": "12.19.0",
45
- "@oh-my-pi/pi-agent-core": "12.19.0",
46
- "@oh-my-pi/pi-ai": "12.19.0",
47
- "@oh-my-pi/pi-natives": "12.19.0",
48
- "@oh-my-pi/pi-tui": "12.19.0",
49
- "@oh-my-pi/pi-utils": "12.19.0",
50
- "@sinclair/typebox": "^0.34.48",
51
- "@xterm/headless": "^6.0.0",
52
- "ajv": "^8.18.0",
53
- "chalk": "^5.6.2",
54
- "diff": "^8.0.3",
55
- "file-type": "^21.3.0",
56
- "glob": "^13.0.3",
57
- "handlebars": "^4.7.8",
58
- "ignore": "^7.0.5",
59
- "linkedom": "^0.18.12",
60
- "marked": "^17.0.2",
61
- "node-html-parser": "^7.0.2",
62
- "puppeteer": "^24.37.3",
63
- "smol-toml": "^1.6.0",
64
- "zod": "^4.3.6"
43
+ "@mozilla/readability": "^0.6",
44
+ "@oh-my-pi/omp-stats": "12.19.3",
45
+ "@oh-my-pi/pi-agent-core": "12.19.3",
46
+ "@oh-my-pi/pi-ai": "12.19.3",
47
+ "@oh-my-pi/pi-natives": "12.19.3",
48
+ "@oh-my-pi/pi-tui": "12.19.3",
49
+ "@oh-my-pi/pi-utils": "12.19.3",
50
+ "@sinclair/typebox": "^0.34",
51
+ "@xterm/headless": "^6.0",
52
+ "ajv": "^8.18",
53
+ "chalk": "^5.6",
54
+ "diff": "^8.0",
55
+ "file-type": "^21.3",
56
+ "glob": "^13.0",
57
+ "handlebars": "^4.7",
58
+ "ignore": "^7.0",
59
+ "linkedom": "^0.18",
60
+ "marked": "^17.0",
61
+ "node-html-parser": "^7.0",
62
+ "puppeteer": "^24.37",
63
+ "smol-toml": "^1.6",
64
+ "zod": "^4.3"
65
65
  },
66
66
  "devDependencies": {
67
- "@types/bun": "^1.3.9",
68
- "@types/ms": "^2.1.0",
69
- "ms": "^2.1.3"
67
+ "@types/bun": "^1.3",
68
+ "@types/ms": "^2.1",
69
+ "ms": "^2.1"
70
70
  },
71
71
  "engines": {
72
72
  "bun": ">=1.3.7"
@@ -87,6 +87,14 @@
87
87
  "types": "./src/*.ts",
88
88
  "import": "./src/*.ts"
89
89
  },
90
+ "./async": {
91
+ "types": "./src/async/index.ts",
92
+ "import": "./src/async/index.ts"
93
+ },
94
+ "./async/*": {
95
+ "types": "./src/async/*.ts",
96
+ "import": "./src/async/*.ts"
97
+ },
90
98
  "./capability": {
91
99
  "types": "./src/capability/index.ts",
92
100
  "import": "./src/capability/index.ts"
@@ -756,17 +756,6 @@ export const SETTINGS_SCHEMA = {
756
756
  // ─────────────────────────────────────────────────────────────────────────
757
757
  // Bash interceptor settings
758
758
  // ─────────────────────────────────────────────────────────────────────────
759
- "bash.virtualTerminal": {
760
- type: "enum",
761
- values: ["on", "off"] as const,
762
- default: "off",
763
- ui: {
764
- tab: "bash",
765
- label: "Virtual terminal",
766
- description: "Use PTY-backed interactive execution for bash",
767
- submenu: true,
768
- },
769
- },
770
759
  "bashInterceptor.enabled": {
771
760
  type: "boolean",
772
761
  default: false,
package/src/main.ts CHANGED
@@ -538,7 +538,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
538
538
  const cwd = getProjectDir();
539
539
  await logger.timeAsync("settings:init", () => Settings.init({ cwd }));
540
540
  if (parsedArgs.noPty) {
541
- settings.override("bash.virtualTerminal", "off");
542
541
  Bun.env.PI_NO_PTY = "1";
543
542
  }
544
543
  const {
@@ -154,11 +154,6 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
154
154
  { value: "tool-only", label: "tool-only", description: "Interrupt only on tool-call argument matches" },
155
155
  { value: "never", label: "never", description: "Never interrupt; inject warning after completion" },
156
156
  ],
157
- // Virtual terminal
158
- "bash.virtualTerminal": [
159
- { value: "on", label: "On", description: "PTY-backed interactive execution" },
160
- { value: "off", label: "Off", description: "Standard non-interactive execution" },
161
- ],
162
157
  // Provider options
163
158
  "providers.webSearch": [
164
159
  {
@@ -21,159 +21,6 @@ export type HashlineEdit =
21
21
  | { op: "append"; after?: LineTag; content: string[] }
22
22
  | { op: "prepend"; before?: LineTag; content: string[] }
23
23
  | { op: "insert"; after: LineTag; before: LineTag; content: string[] };
24
- export type ReplaceTextEdit = { op: "replaceText"; old_text: string; new_text: string; all?: boolean };
25
- export type EditSpec = HashlineEdit | ReplaceTextEdit;
26
-
27
- /**
28
- * Compare two strings ignoring all whitespace differences.
29
- *
30
- * Returns true when the non-whitespace characters are identical — meaning
31
- * the only differences are in spaces, tabs, or other whitespace.
32
- */
33
- function equalsIgnoringWhitespace(a: string, b: string): boolean {
34
- // Fast path: identical strings
35
- if (a === b) return true;
36
- // Compare with all whitespace removed
37
- return a.replace(/\s+/g, "") === b.replace(/\s+/g, "");
38
- }
39
-
40
- function stripAllWhitespace(s: string): string {
41
- return s.replace(/\s+/g, "");
42
- }
43
-
44
- function stripTrailingContinuationTokens(s: string): string {
45
- // Heuristic: models often merge a continuation line into the prior line
46
- // while also changing the trailing operator (e.g. `&&` → `||`).
47
- // Strip common trailing continuation tokens so we can still detect merges.
48
- return s.replace(/(?:&&|\|\||\?\?|\?|:|=|,|\+|-|\*|\/|\.|\()\s*$/u, "");
49
- }
50
-
51
- function stripMergeOperatorChars(s: string): string {
52
- // Used for merge detection when the model changes a logical operator like
53
- // `||` → `??` while also merging adjacent lines.
54
- return s.replace(/[|&?]/g, "");
55
- }
56
-
57
- function leadingWhitespace(s: string): string {
58
- const match = s.match(/^\s*/);
59
- return match ? match[0] : "";
60
- }
61
-
62
- function restoreLeadingIndent(templateLine: string, line: string): string {
63
- if (line.length === 0) return line;
64
- const templateIndent = leadingWhitespace(templateLine);
65
- if (templateIndent.length === 0) return line;
66
- const indent = leadingWhitespace(line);
67
- if (indent.length > 0) return line;
68
- return templateIndent + line;
69
- }
70
-
71
- function restoreIndentForPairedReplacement(oldLines: string[], newLines: string[]): string[] {
72
- if (oldLines.length !== newLines.length) return newLines;
73
- let changed = false;
74
- const out = new Array<string>(newLines.length);
75
- for (let i = 0; i < newLines.length; i++) {
76
- const restored = restoreLeadingIndent(oldLines[i], newLines[i]);
77
- out[i] = restored;
78
- if (restored !== newLines[i]) changed = true;
79
- }
80
- return changed ? out : newLines;
81
- }
82
-
83
- /**
84
- * Undo pure formatting rewrites where the model reflows a single logical line
85
- * into multiple lines (or similar), but the token stream is identical.
86
- */
87
- function restoreOldWrappedLines(oldLines: string[], newLines: string[]): string[] {
88
- if (oldLines.length === 0 || newLines.length < 2) return newLines;
89
-
90
- const canonToOld = new Map<string, { line: string; count: number }>();
91
- for (const line of oldLines) {
92
- const canon = stripAllWhitespace(line);
93
- const bucket = canonToOld.get(canon);
94
- if (bucket) bucket.count++;
95
- else canonToOld.set(canon, { line, count: 1 });
96
- }
97
-
98
- const candidates: { start: number; len: number; replacement: string; canon: string }[] = [];
99
- for (let start = 0; start < newLines.length; start++) {
100
- for (let len = 2; len <= 10 && start + len <= newLines.length; len++) {
101
- const canonSpan = stripAllWhitespace(newLines.slice(start, start + len).join(""));
102
- const old = canonToOld.get(canonSpan);
103
- if (old && old.count === 1 && canonSpan.length >= 6) {
104
- candidates.push({ start, len, replacement: old.line, canon: canonSpan });
105
- }
106
- }
107
- }
108
- if (candidates.length === 0) return newLines;
109
-
110
- // Keep only spans whose canonical match is unique in the new output.
111
- const canonCounts = new Map<string, number>();
112
- for (const c of candidates) {
113
- canonCounts.set(c.canon, (canonCounts.get(c.canon) ?? 0) + 1);
114
- }
115
- const uniqueCandidates = candidates.filter(c => (canonCounts.get(c.canon) ?? 0) === 1);
116
- if (uniqueCandidates.length === 0) return newLines;
117
-
118
- // Apply replacements back-to-front so indices remain stable.
119
- uniqueCandidates.sort((a, b) => b.start - a.start);
120
- const out = [...newLines];
121
- for (const c of uniqueCandidates) {
122
- out.splice(c.start, c.len, c.replacement);
123
- }
124
- return out;
125
- }
126
-
127
- function stripInsertAnchorEchoAfter(anchorLine: string, dstLines: string[]): string[] {
128
- if (dstLines.length <= 1) return dstLines;
129
- if (equalsIgnoringWhitespace(dstLines[0], anchorLine)) {
130
- return dstLines.slice(1);
131
- }
132
- return dstLines;
133
- }
134
-
135
- function stripInsertAnchorEchoBefore(anchorLine: string, dstLines: string[]): string[] {
136
- if (dstLines.length <= 1) return dstLines;
137
- if (equalsIgnoringWhitespace(dstLines[dstLines.length - 1], anchorLine)) {
138
- return dstLines.slice(0, -1);
139
- }
140
- return dstLines;
141
- }
142
-
143
- function stripInsertBoundaryEcho(afterLine: string, beforeLine: string, dstLines: string[]): string[] {
144
- let out = dstLines;
145
- if (out.length > 1 && equalsIgnoringWhitespace(out[0], afterLine)) {
146
- out = out.slice(1);
147
- }
148
- if (out.length > 1 && equalsIgnoringWhitespace(out[out.length - 1], beforeLine)) {
149
- out = out.slice(0, -1);
150
- }
151
- return out;
152
- }
153
-
154
- function stripRangeBoundaryEcho(fileLines: string[], startLine: number, endLine: number, dstLines: string[]): string[] {
155
- // Only strip when the model replaced with multiple lines and grew the edit.
156
- // This avoids turning a single-line replacement into a deletion.
157
- const count = endLine - startLine + 1;
158
- if (dstLines.length <= 1 || dstLines.length <= count) return dstLines;
159
-
160
- let out = dstLines;
161
- const beforeIdx = startLine - 2;
162
- if (beforeIdx >= 0 && equalsIgnoringWhitespace(out[0], fileLines[beforeIdx])) {
163
- out = out.slice(1);
164
- }
165
-
166
- const afterIdx = endLine;
167
- if (
168
- afterIdx < fileLines.length &&
169
- out.length > 0 &&
170
- equalsIgnoringWhitespace(out[out.length - 1], fileLines[afterIdx])
171
- ) {
172
- out = out.slice(0, -1);
173
- }
174
-
175
- return out;
176
- }
177
24
 
178
25
  const NIBBLE_STR = "ZPMQVRWSNKTXJBYH";
179
26
 
@@ -594,38 +441,6 @@ export function applyHashlineEdits(
594
441
  let firstChangedLine: number | undefined;
595
442
  const noopEdits: Array<{ editIndex: number; loc: string; currentContent: string }> = [];
596
443
 
597
- const autocorrect = Bun.env.PI_HL_AUTOCORRECT === "1";
598
-
599
- function collectExplicitlyTouchedLines(): Set<number> {
600
- const touched = new Set<number>();
601
- for (const edit of edits) {
602
- switch (edit.op) {
603
- case "set":
604
- touched.add(edit.tag.line);
605
- break;
606
- case "replace":
607
- for (let ln = edit.first.line; ln <= edit.last.line; ln++) touched.add(ln);
608
- break;
609
- case "append":
610
- if (edit.after) {
611
- touched.add(edit.after.line);
612
- }
613
- break;
614
- case "prepend":
615
- if (edit.before) {
616
- touched.add(edit.before.line);
617
- }
618
- break;
619
- case "insert":
620
- touched.add(edit.after.line);
621
- touched.add(edit.before.line);
622
- break;
623
- }
624
- }
625
- return touched;
626
- }
627
-
628
- const explicitlyTouchedLines = collectExplicitlyTouchedLines();
629
444
  // Pre-validate: collect all hash mismatches before mutating
630
445
  const mismatches: HashMismatch[] = [];
631
446
  function validateRef(ref: { line: number; hash: string }): boolean {
@@ -765,35 +580,8 @@ export function applyHashlineEdits(
765
580
  for (const { edit, idx } of annotated) {
766
581
  switch (edit.op) {
767
582
  case "set": {
768
- const merged = autocorrect ? maybeExpandSingleLineMerge(edit.tag.line, edit.content) : null;
769
- if (merged) {
770
- const origLines = originalFileLines.slice(
771
- merged.startLine - 1,
772
- merged.startLine - 1 + merged.deleteCount,
773
- );
774
- let nextLines = merged.newLines;
775
- nextLines = restoreIndentForPairedReplacement([origLines[0] ?? ""], nextLines);
776
-
777
- if (origLines.every((line, i) => line === nextLines[i])) {
778
- noopEdits.push({
779
- editIndex: idx,
780
- loc: `${edit.tag.line}#${edit.tag.hash}`,
781
- currentContent: origLines.join("\n"),
782
- });
783
- break;
784
- }
785
- fileLines.splice(merged.startLine - 1, merged.deleteCount, ...nextLines);
786
- trackFirstChanged(merged.startLine);
787
- break;
788
- }
789
-
790
- const count = 1;
791
583
  const origLines = originalFileLines.slice(edit.tag.line - 1, edit.tag.line);
792
- let stripped = autocorrect
793
- ? stripRangeBoundaryEcho(originalFileLines, edit.tag.line, edit.tag.line, edit.content)
794
- : edit.content;
795
- stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
796
- const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
584
+ const newLines = edit.content;
797
585
  if (origLines.every((line, i) => line === newLines[i])) {
798
586
  noopEdits.push({
799
587
  editIndex: idx,
@@ -802,36 +590,19 @@ export function applyHashlineEdits(
802
590
  });
803
591
  break;
804
592
  }
805
- fileLines.splice(edit.tag.line - 1, count, ...newLines);
593
+ fileLines.splice(edit.tag.line - 1, 1, ...newLines);
806
594
  trackFirstChanged(edit.tag.line);
807
595
  break;
808
596
  }
809
597
  case "replace": {
810
598
  const count = edit.last.line - edit.first.line + 1;
811
- const origLines = originalFileLines.slice(edit.first.line - 1, edit.first.line - 1 + count);
812
- let stripped = autocorrect
813
- ? stripRangeBoundaryEcho(originalFileLines, edit.first.line, edit.last.line, edit.content)
814
- : edit.content;
815
- stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
816
- const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
817
- if (autocorrect && origLines.every((line, i) => line === newLines[i])) {
818
- noopEdits.push({
819
- editIndex: idx,
820
- loc: `${edit.first.line}#${edit.first.hash}`,
821
- currentContent: origLines.join("\n"),
822
- });
823
- break;
824
- }
599
+ const newLines = edit.content;
825
600
  fileLines.splice(edit.first.line - 1, count, ...newLines);
826
601
  trackFirstChanged(edit.first.line);
827
602
  break;
828
603
  }
829
604
  case "append": {
830
- const inserted = edit.after
831
- ? autocorrect
832
- ? stripInsertAnchorEchoAfter(originalFileLines[edit.after.line - 1], edit.content)
833
- : edit.content
834
- : edit.content;
605
+ const inserted = edit.content;
835
606
  if (inserted.length === 0) {
836
607
  noopEdits.push({
837
608
  editIndex: idx,
@@ -855,11 +626,7 @@ export function applyHashlineEdits(
855
626
  break;
856
627
  }
857
628
  case "prepend": {
858
- const inserted = edit.before
859
- ? autocorrect
860
- ? stripInsertAnchorEchoBefore(originalFileLines[edit.before.line - 1], edit.content)
861
- : edit.content
862
- : edit.content;
629
+ const inserted = edit.content;
863
630
  if (inserted.length === 0) {
864
631
  noopEdits.push({
865
632
  editIndex: idx,
@@ -884,7 +651,7 @@ export function applyHashlineEdits(
884
651
  case "insert": {
885
652
  const afterLine = originalFileLines[edit.after.line - 1];
886
653
  const beforeLine = originalFileLines[edit.before.line - 1];
887
- const inserted = autocorrect ? stripInsertBoundaryEcho(afterLine, beforeLine, edit.content) : edit.content;
654
+ const inserted = edit.content;
888
655
  if (inserted.length === 0) {
889
656
  noopEdits.push({
890
657
  editIndex: idx,
@@ -911,51 +678,4 @@ export function applyHashlineEdits(
911
678
  firstChangedLine = line;
912
679
  }
913
680
  }
914
-
915
- function maybeExpandSingleLineMerge(
916
- line: number,
917
- content: string[],
918
- ): { startLine: number; deleteCount: number; newLines: string[] } | null {
919
- if (content.length !== 1) return null;
920
- if (line < 1 || line > fileLines.length) return null;
921
-
922
- const newLine = content[0];
923
- const newCanon = stripAllWhitespace(newLine);
924
- const newCanonForMergeOps = stripMergeOperatorChars(newCanon);
925
- if (newCanon.length === 0) return null;
926
-
927
- const orig = fileLines[line - 1];
928
- const origCanon = stripAllWhitespace(orig);
929
- const origCanonForMatch = stripTrailingContinuationTokens(origCanon);
930
- const origCanonForMergeOps = stripMergeOperatorChars(origCanon);
931
- const origLooksLikeContinuation = origCanonForMatch.length < origCanon.length;
932
- if (origCanon.length === 0) return null;
933
- const nextIdx = line;
934
- const prevIdx = line - 2;
935
- // Case A: dst absorbed the next continuation line.
936
- if (origLooksLikeContinuation && nextIdx < fileLines.length && !explicitlyTouchedLines.has(line + 1)) {
937
- const next = fileLines[nextIdx];
938
- const nextCanon = stripAllWhitespace(next);
939
- const a = newCanon.indexOf(origCanonForMatch);
940
- const b = newCanon.indexOf(nextCanon);
941
- if (a !== -1 && b !== -1 && a < b && newCanon.length <= origCanon.length + nextCanon.length + 32) {
942
- return { startLine: line, deleteCount: 2, newLines: [newLine] };
943
- }
944
- }
945
- // Case B: dst absorbed the previous declaration/continuation line.
946
- if (prevIdx >= 0 && !explicitlyTouchedLines.has(line - 1)) {
947
- const prev = fileLines[prevIdx];
948
- const prevCanon = stripAllWhitespace(prev);
949
- const prevCanonForMatch = stripTrailingContinuationTokens(prevCanon);
950
- const prevLooksLikeContinuation = prevCanonForMatch.length < prevCanon.length;
951
- if (!prevLooksLikeContinuation) return null;
952
- const a = newCanonForMergeOps.indexOf(stripMergeOperatorChars(prevCanonForMatch));
953
- const b = newCanonForMergeOps.indexOf(origCanonForMergeOps);
954
- if (a !== -1 && b !== -1 && a < b && newCanon.length <= prevCanon.length + origCanon.length + 32) {
955
- return { startLine: line - 1, deleteCount: 2, newLines: [newLine] };
956
- }
957
- }
958
-
959
- return null;
960
- }
961
681
  }
@@ -34,14 +34,7 @@ import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard"
34
34
  import { applyPatch } from "./applicator";
35
35
  import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
36
36
  import { findMatch } from "./fuzzy";
37
- import {
38
- applyHashlineEdits,
39
- computeLineHash,
40
- type HashlineEdit,
41
- type LineTag,
42
- parseTag,
43
- type ReplaceTextEdit,
44
- } from "./hashline";
37
+ import { applyHashlineEdits, computeLineHash, type HashlineEdit, type LineTag, parseTag } from "./hashline";
45
38
  import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
46
39
  import { buildNormativeUpdateInput } from "./normative";
47
40
  import { type EditToolDetails, getLspBatchRequest } from "./shared";
@@ -201,13 +194,6 @@ export function hashlineParseContent(edit: string | string[] | null): string[] {
201
194
  if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
202
195
  return lines;
203
196
  }
204
-
205
- function hashlineParseContentString(edit: string | string[] | null): string {
206
- if (edit === null) return "";
207
- if (Array.isArray(edit)) return edit.join("\n");
208
- return edit;
209
- }
210
-
211
197
  const hashlineTargetEditSchema = Type.Object(
212
198
  {
213
199
  op: Type.Literal("set"),
@@ -255,25 +241,12 @@ const hashlineInsertEditSchema = Type.Object(
255
241
  { additionalProperties: false },
256
242
  );
257
243
 
258
- const hashlineReplaceTextEditSchema = Type.Object(
259
- {
260
- op: Type.Literal("replaceText"),
261
- old_text: Type.String({ description: "Text to find", minLength: 1 }),
262
- new_text: hashlineReplaceContentFormat("Replacement"),
263
- all: Type.Optional(Type.Boolean({ description: "Replace all occurrences" })),
264
- },
265
- { additionalProperties: false },
266
- );
267
-
268
- const HL_REPLACE_ENABLED = Bun.env.PI_HL_REPLACETXT === "1";
269
-
270
244
  const hashlineEditSpecSchema = Type.Union([
271
245
  hashlineTargetEditSchema,
272
246
  hashlineRangeEditSchema,
273
247
  hashlineAppendEditSchema,
274
248
  hashlinePrependEditSchema,
275
249
  hashlineInsertEditSchema,
276
- ...(HL_REPLACE_ENABLED ? [hashlineReplaceTextEditSchema] : []),
277
250
  ]);
278
251
 
279
252
  const hashlineEditSchema = Type.Object(
@@ -486,7 +459,7 @@ export class EditTool implements AgentTool<TInput> {
486
459
  case "patch":
487
460
  return renderPromptTemplate(patchDescription);
488
461
  case "hashline":
489
- return renderPromptTemplate(hashlineDescription, { allowReplaceText: HL_REPLACE_ENABLED });
462
+ return renderPromptTemplate(hashlineDescription);
490
463
  default:
491
464
  return renderPromptTemplate(replaceDescription);
492
465
  }
@@ -581,7 +554,6 @@ export class EditTool implements AgentTool<TInput> {
581
554
  }
582
555
 
583
556
  const anchorEdits: HashlineEdit[] = [];
584
- const replaceEdits: ReplaceTextEdit[] = [];
585
557
  for (const edit of edits) {
586
558
  switch (edit.op) {
587
559
  case "set": {
@@ -643,16 +615,6 @@ export class EditTool implements AgentTool<TInput> {
643
615
  }
644
616
  break;
645
617
  }
646
- case "replaceText": {
647
- const { old_text, new_text, all } = edit;
648
- replaceEdits.push({
649
- op: "replaceText",
650
- old_text: old_text,
651
- new_text: hashlineParseContentString(new_text),
652
- all: all ?? false,
653
- });
654
- break;
655
- }
656
618
  default:
657
619
  throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
658
620
  }
@@ -668,19 +630,6 @@ export class EditTool implements AgentTool<TInput> {
668
630
  const anchorResult = applyHashlineEdits(normalizedContent, anchorEdits);
669
631
  normalizedContent = anchorResult.content;
670
632
 
671
- // Apply content-replace edits (substr-style fuzzy replace)
672
- for (const r of replaceEdits) {
673
- if (r.old_text.length === 0) {
674
- throw new Error("old_text must not be empty.");
675
- }
676
- const rep = replaceText(normalizedContent, r.old_text, r.new_text, {
677
- fuzzy: this.#allowFuzzy,
678
- all: r.all ?? false,
679
- threshold: this.#fuzzyThreshold,
680
- });
681
- normalizedContent = rep.content;
682
- }
683
-
684
633
  const result = {
685
634
  content: normalizedContent,
686
635
  firstChangedLine: anchorResult.firstChangedLine,
@@ -22,10 +22,6 @@ Apply precise file edits using `LINE#ID` tags, anchoring to the file content.
22
22
  - `{ op: "prepend", before: "N#ID", content: […] }` or `{ op: "prepend", content: […] }` (no `before` = insert at beginning of file)
23
23
  - `{ op: "append", after: "N#ID", content: […] }` or `{ op: "append", content: […] }` (no `after` = insert at end of file)
24
24
  - `{ op: "insert", after: "N#ID", before: "N#ID", content: […] }` (between adjacent anchors; safest for blocks)
25
- {{#if allowReplaceText}}
26
- - **Content replace**
27
- - `{ op: "replaceText", old_text: "…", new_text: "…", all?: boolean }`
28
- {{/if}}
29
25
  - **File-level controls**
30
26
  - `{ delete: true, edits: [] }` deletes the file (cannot be combined with `rename`).
31
27
  - `{ rename: "new/path.ts", edits: […] }` writes result to new path and removes old path.
@@ -180,18 +176,6 @@ content: ["function validate(data: unknown): boolean {", " return data != null
180
176
  The trailing `""` in `content` preserves the blank-line separator. **Anchor to the structural line (`export function ...`), not the blank line above it** — blank lines are ambiguous and may be added or removed by other edits.
181
177
  </example>
182
178
 
183
- {{#if allowReplaceText}}
184
- <example name="content replace (rare)">
185
- ```
186
- op: "replaceText"
187
- old_text: "x = 42"
188
- new_text: "x = 99"
189
- ```
190
-
191
- Use only when line anchors aren't available. `old_text` must match exactly one location in the file (or set `"all": true` for all occurrences).
192
- </example>
193
- {{/if}}
194
-
195
179
  <example name="file delete">
196
180
  ```
197
181
  path: "src/deprecated/legacy.ts"
@@ -2224,6 +2224,7 @@ export class AgentSession {
2224
2224
 
2225
2225
  this.#disconnectFromAgent();
2226
2226
  await this.abort();
2227
+ this.#asyncJobManager?.cancelAll();
2227
2228
  this.agent.reset();
2228
2229
  await this.sessionManager.flush();
2229
2230
  await this.sessionManager.newSession(options);
@@ -2933,6 +2934,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
2933
2934
 
2934
2935
  // Start a new session
2935
2936
  await this.sessionManager.flush();
2937
+ this.#asyncJobManager?.cancelAll();
2936
2938
  await this.sessionManager.newSession();
2937
2939
  this.agent.reset();
2938
2940
  this.agent.sessionId = this.sessionManager.getSessionId();
@@ -4126,6 +4128,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
4126
4128
 
4127
4129
  // Flush pending writes before branching
4128
4130
  await this.sessionManager.flush();
4131
+ this.#asyncJobManager?.cancelAll();
4129
4132
 
4130
4133
  if (!selectedEntry.parentId) {
4131
4134
  await this.sessionManager.newSession({ parentSession: previousSessionFile });
package/src/tools/bash.ts CHANGED
@@ -34,6 +34,11 @@ const bashSchemaBase = Type.Object({
34
34
  cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
35
35
  head: Type.Optional(Type.Number({ description: "Return only first N lines of output" })),
36
36
  tail: Type.Optional(Type.Number({ description: "Return only last N lines of output" })),
37
+ pty: Type.Optional(
38
+ Type.Boolean({
39
+ description: "Run in PTY mode when command needs a real terminal (e.g. sudo/ssh/top/less); default: false",
40
+ }),
41
+ ),
37
42
  });
38
43
 
39
44
  const bashSchemaWithAsync = Type.Object({
@@ -54,6 +59,7 @@ export interface BashToolInput {
54
59
  head?: number;
55
60
  tail?: number;
56
61
  async?: boolean;
62
+ pty?: boolean;
57
63
  }
58
64
 
59
65
  export interface BashToolDetails {
@@ -123,7 +129,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
123
129
 
124
130
  async execute(
125
131
  _toolCallId: string,
126
- { command: rawCommand, timeout: rawTimeout = 300, cwd, head, tail, async: asyncRequested = false }: BashToolInput,
132
+ {
133
+ command: rawCommand,
134
+ timeout: rawTimeout = 300,
135
+ cwd,
136
+ head,
137
+ tail,
138
+ async: asyncRequested = false,
139
+ pty = false,
140
+ }: BashToolInput,
127
141
  signal?: AbortSignal,
128
142
  onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
129
143
  ctx?: AgentToolContext,
@@ -188,7 +202,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
188
202
  try {
189
203
  const result = await executeBash(command, {
190
204
  cwd: commandCwd,
191
- sessionKey: this.session.getSessionId?.() ?? undefined,
205
+ sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
192
206
  timeout: timeoutMs,
193
207
  signal: runSignal,
194
208
  env: extraEnv,
@@ -229,11 +243,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
229
243
  const extraEnv = artifactsDir ? { ARTIFACTS: artifactsDir } : undefined;
230
244
  const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
231
245
 
232
- const usePty =
233
- this.session.settings.get("bash.virtualTerminal") === "on" &&
234
- $env.PI_NO_PTY !== "1" &&
235
- ctx?.hasUI === true &&
236
- ctx.ui !== undefined;
246
+ const usePty = pty && $env.PI_NO_PTY !== "1" && ctx?.hasUI === true && ctx.ui !== undefined;
237
247
  const result: BashResult | BashInteractiveResult = usePty
238
248
  ? await runInteractiveBashPty(ctx.ui!, {
239
249
  command,