@oh-my-pi/pi-coding-agent 14.4.0 → 14.4.1

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.
@@ -10,6 +10,7 @@ import {
10
10
  truncateToWidth,
11
11
  visibleWidth,
12
12
  } from "@oh-my-pi/pi-tui";
13
+ import { formatBytes } from "@oh-my-pi/pi-utils";
13
14
  import { theme } from "../../modes/theme/theme";
14
15
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
15
16
  import type { SessionInfo } from "../../session/session-manager";
@@ -157,10 +158,9 @@ class SessionList implements Component {
157
158
  lines.push(messageLine);
158
159
  }
159
160
 
160
- // Metadata line: date + message count
161
+ // Metadata line: date + file size
161
162
  const modified = formatDate(session.modified);
162
- const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
163
- const metadata = ` ${modified} ${theme.sep.dot} ${msgCount}`;
163
+ const metadata = ` ${modified} ${theme.sep.dot} ${formatBytes(session.size)}`;
164
164
  const metadataLine = theme.fg("dim", truncateToWidth(metadata, width));
165
165
 
166
166
  lines.push(metadataLine);
@@ -14,7 +14,7 @@ Performs structural AST-aware rewrites via native ast-grep.
14
14
  </instruction>
15
15
 
16
16
  <output>
17
- - Replacement summary, per-file replacement counts, and change diffs
17
+ - Replacement summary, per-file replacement counts, and change diffs as `-LINE+ID|before` / `+LINE+ID|after` lines
18
18
  - Parse issues when files cannot be processed
19
19
  </output>
20
20
 
@@ -18,6 +18,7 @@ Performs structural code search using AST matching via native ast-grep.
18
18
 
19
19
  <output>
20
20
  - Grouped matches with file path, byte range, line/column ranges, metavariable captures
21
+ - Match lines are anchor-prefixed: `LINE+ID>content` for the matched line and `LINE+ID:content` for surrounding context
21
22
  - Summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
22
23
  </output>
23
24
 
@@ -15,6 +15,10 @@ Verbs:
15
15
  - `set: ["…"]` — replace the anchor line
16
16
  - `pre: ["…"]` — insert before the anchor line (or at BOF when `loc:"^"`)
17
17
  - `post: ["…"]` — insert after the anchor line (or at EOF when `loc:"$"`)
18
+ - `sed: "s/foo/bar/"` — sed-style substitution applied to the anchor line. **Prefer this over `set` for token-level changes**
19
+ Flags: `g` (all occurrences), `i` (case-insensitive), `F` (literal/fixed-string, no regex).
20
+ Delimiter is whatever character follows `s`.
21
+ You **MUST** keep the pattern as short as possible.
18
22
 
19
23
  Combination rules:
20
24
  - On a single-anchor `loc`, you may combine `pre`, `set`, and `post` in the same entry.
@@ -26,60 +30,45 @@ Combination rules:
26
30
  All examples below reference the same file:
27
31
 
28
32
  ```ts title="a.ts"
29
- {{hline 1 "// @ts-ignore"}}
30
- {{hline 2 "const timeout = 5000;"}}
31
- {{hline 3 "const tag = \"DO NOT SHIP\";"}}
32
- {{hline 4 "const fallback = group.targetFramework || 'All Frameworks';"}}
33
- {{hline 5 "function alpha() {"}}
34
- {{hline 6 "\tlog();"}}
35
- {{hline 7 "}"}}
36
- {{hline 8 ""}}
37
- {{hline 9 "function beta(x) {"}}
38
- {{hline 10 "\tif (x) {"}}
39
- {{hline 11 "\t\treturn parse(data);"}}
40
- {{hline 12 "\t}"}}
41
- {{hline 13 "\treturn null;"}}
42
- {{hline 14 "}"}}
33
+ {{hline 1 "const tag = \"BAD\";"}}
34
+ {{hline 2 ""}}
35
+ {{hline 3 "function beta(x) {"}}
36
+ {{hline 4 "\tif (x) {"}}
37
+ {{hline 5 "\t\treturn parse(data) || fallback;"}}
38
+ {{hline 6 "\t}"}}
39
+ {{hline 7 "\treturn null;"}}
40
+ {{hline 8 "}"}}
43
41
  ```
44
42
 
45
- # Swap an operator by replacing the line
46
- Original line 4: `const fallback = group.targetFramework || 'All Frameworks';`
47
- `{path:"a.ts",edits:[{loc:{{href 4 "const fallback = group.targetFramework || 'All Frameworks';"}},set:["const fallback = group.targetFramework ?? 'All Frameworks';"]}]}`
48
-
49
- # Flip a literal by replacing the line
50
- Original line 2: `const timeout = 5000;`
51
- `{path:"a.ts",edits:[{loc:{{href 2 "const timeout = 5000;"}},set:["const timeout = 30_000;"]}]}`
52
-
53
- # Negate a condition by replacing the line
54
- Original line 10: `\tif (x) {`
55
- `{path:"a.ts",edits:[{loc:{{href 10 "\tif (x) {"}},set:["\tif (!x) {"]}]}`
43
+ # Replace a line with `set`
44
+ `{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},set:["const tag = \"OK\";"]}]}`
56
45
 
57
46
  # Combine `pre` + `set` + `post` in one entry
58
- `{path:"a.ts",edits:[{loc:{{href 6 "\tlog();"}},pre:["\tvalidate();"],set:["\tlog();"],post:["\tcleanup();"]}]}`
59
-
60
- # Replace one whole line with `set`
61
- Use `set` to replace the full anchored line, preserving any unchanged surrounding lines yourself.
62
- `{path:"a.ts",edits:[{loc:{{href 3 "const tag = \"DO NOT SHIP\";"}},set:["const tag = \"OK\";"]}]}`
63
-
64
- # Replace multiple non-adjacent lines
65
- `{path:"a.ts",edits:[{loc:{{href 11 "\t\treturn parse(data);"}},set:["\t\treturn parse(data) ?? fallback;"]},{loc:{{href 13 "\treturn null;"}},set:["\treturn fallback;"]}]}`
47
+ `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],set:["\tif (!x) {"],post:["\t\tlog();"]}]}`
66
48
 
67
49
  # Delete a line with `set: []`
68
- `{path:"a.ts",edits:[{loc:{{href 11 "\t\treturn parse(data);"}},set:[]}]}`
50
+ `{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},set:[]}]}`
69
51
 
70
52
  # Preserve a blank line with `set:[""]`
71
- `{path:"a.ts",edits:[{loc:{{href 8 ""}},set:[""]}]}`
53
+ `{path:"a.ts",edits:[{loc:{{href 2 ""}},set:[""]}]}`
72
54
 
73
55
  # Insert before / after a line
74
- `{path:"a.ts",edits:[{loc:{{href 9 "function beta(x) {"}},pre:["function gamma() {","\tvalidate();","}",""]}]}`
75
- `{path:"a.ts",edits:[{loc:{{href 6 "\tlog();"}},post:["\tvalidate();"]}]}`
56
+ `{path:"a.ts",edits:[{loc:{{href 3 "function beta(x) {"}},pre:["function gamma() {","\tvalidate();","}",""]}]}`
57
+
58
+ # Substitute one token with `sed` (regex) — preferred for token-level edits
59
+ Use the smallest pattern that uniquely identifies the change.
60
+ `{path:"a.ts",edits:[{loc:{{href 5 "\t\treturn parse(data) || fallback;"}},sed:"s/\\|\\|/??/"}]}`
61
+
62
+ # Substitute every occurrence with `sed` (literal/fixed-string)
63
+ Use the `F` flag to disable regex; the delimiter can be any non-alphanumeric char.
64
+ `{path:"a.ts",edits:[{loc:{{href 5 "\t\treturn parse(data) || fallback;"}},sed:"s|data|input|gF"}]}`
76
65
 
77
66
  # Prepend / append at file edges
78
67
  `{path:"a.ts",edits:[{loc:"^",pre:["// Copyright (c) 2026",""]}]}`
79
68
  `{path:"a.ts",edits:[{loc:"$",post:["","export const VERSION = \"1.0.0\";"]}]}`
80
69
 
81
70
  # Cross-file override inside `loc`
82
- `{path:"a.ts",edits:[{loc:"b.ts:{{href 2 "const timeout = 5000;"}}",set:["const timeout = 30_000;"]}]}`
71
+ `{path:"a.ts",edits:[{loc:"b.ts:{{href 1 "const tag = \"BAD\";"}}",set:["const tag = \"OK\";"]}]}`
83
72
  </examples>
84
73
 
85
74
  <critical>
@@ -91,6 +80,8 @@ Use `set` to replace the full anchored line, preserving any unchanged surroundin
91
80
  - `set: []` deletes the anchored line. `set:[""]` preserves a blank line.
92
81
  - Within a single request you may submit edits in any order — the runtime applies them bottom-up so they don't shift each other. After any request that mutates a file, anchors below the mutation are stale on disk; re-read before issuing more edits to that file.
93
82
  - `set` operations target the current file content only. Do not try to reference old line text after the file has changed.
83
+ - For token-level edits, prefer `sed` over `set`. The `loc` anchor already pins the line — repeating the entire line in a `set` array invites hallucinated content. Use the smallest `sed` pattern that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe.
84
+ - When you do use `set`, re-read the anchored line first and copy it verbatim, changing only the required token(s). Anchor identity does not verify line content, so a hallucinated replacement will silently corrupt the file.
94
85
  - Text content must be literal file content with matching indentation. If the file uses tabs, use real tabs.
95
86
  - You **MUST NOT** use this tool to reformat or clean up unrelated code.
96
87
  </critical>
@@ -8,14 +8,14 @@ Searches files using powerful regex matching.
8
8
 
9
9
  <output>
10
10
  {{#if IS_HASHLINE_MODE}}
11
- - Text output is anchor-prefixed: `123th:content` (match) or `123th-content` (context). The 2-letter ID is a content fingerprint.
11
+ - Text output is anchor-prefixed: `123th>content` (match) or `123th:content` (context). The 2-letter ID is a content fingerprint.
12
12
  {{else}}
13
13
  {{#if IS_LINE_NUMBER_MODE}}
14
14
  - Text output is line-number-prefixed
15
15
  {{/if}}
16
16
  {{/if}}
17
17
  {{#if IS_CHUNK_MODE}}
18
- - Text output is chunk-path-prefixed: `path:sel>123th:content`
18
+ - Text output is chunk-path-prefixed: `path:sel>123|content`
19
19
  {{/if}}
20
20
  </output>
21
21
 
@@ -24,7 +24,7 @@ Max {{DEFAULT_MAX_LINES}} lines per call.
24
24
 
25
25
  # Filesystem
26
26
  {{#if IS_HASHLINE_MODE}}
27
- - Reading from FS returns lines prefixed with anchors: `41th:def alpha():` (line number, 2-letter ID, colon, then content)
27
+ - Reading from FS returns lines prefixed with anchors: `41th|def alpha():` (line number, 2-letter ID, pipe, then content)
28
28
  {{else}}
29
29
  {{#if IS_LINE_NUMBER_MODE}}
30
30
  - Reading from FS returns lines prefixed with line numbers: `41:def alpha():`
@@ -259,6 +259,8 @@ export interface SessionInfo {
259
259
  created: Date;
260
260
  modified: Date;
261
261
  messageCount: number;
262
+ /** File size in bytes on disk; used for compact list rendering. */
263
+ size: number;
262
264
  firstMessage: string;
263
265
  allMessagesText: string;
264
266
  }
@@ -1264,7 +1266,7 @@ function extractTextFromContent(content: Message["content"]): string {
1264
1266
  .join(" ");
1265
1267
  }
1266
1268
 
1267
- const SESSION_LIST_PREFIX_BYTES = 1024;
1269
+ const SESSION_LIST_PREFIX_BYTES = 4096;
1268
1270
  const SESSION_LIST_PARALLEL_THRESHOLD = 64;
1269
1271
  const SESSION_LIST_MAX_WORKERS = 16;
1270
1272
  const sessionListPrefixDecoder = new TextDecoder("utf-8", { fatal: false });
@@ -1466,6 +1468,7 @@ async function collectSessionFromFile(
1466
1468
  created: new Date(header.timestamp ?? ""),
1467
1469
  modified: stats.mtime,
1468
1470
  messageCount,
1471
+ size: stats.size,
1469
1472
  firstMessage: firstMessage || "(no messages)",
1470
1473
  allMessagesText: allMessages.length > 0 ? allMessages.join(" ") : firstMessage,
1471
1474
  };
package/src/tools/grep.ts CHANGED
@@ -64,7 +64,7 @@ export interface GrepToolDetails {
64
64
  truncated?: boolean;
65
65
  error?: string;
66
66
  /** Pre-formatted text for the user-visible TUI render. Mirrors the model-facing
67
- * `result.text` lines but uses a `│` gutter and `*` to mark match lines (vs `-` for
67
+ * `result.text` lines but uses a `│` gutter and `*` to mark match lines (vs space for
68
68
  * context). The TUI uses this directly so it never parses model-facing hashline anchors. */
69
69
  displayContent?: string;
70
70
  }
@@ -502,7 +502,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
502
502
  }
503
503
  }
504
504
  if (hasContextLines && outputLines.length > 0) {
505
- outputLines.unshift("[grep] match lines use ':'; context lines use '-'.");
505
+ outputLines.unshift("[grep] match lines use '>'; context lines use ':'.");
506
506
  }
507
507
  if (matchLimitReached || result.limitReached) {
508
508
  outputLines.push("", limitMessage);
@@ -1,9 +1,9 @@
1
- import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash";
1
+ import { computeLineHash } from "../edit/line-hash";
2
2
 
3
3
  /**
4
4
  * Format a single line of match output for grep/ast-grep style results.
5
5
  *
6
- * Match lines use `:` as the anchor/content separator; context lines use `-`.
6
+ * Match lines use `>` as the anchor/content separator; context lines use `:`.
7
7
  * In hashline mode the anchor is `LINE+ID` (no `#`); in plain mode it is
8
8
  * just the line number. Line numbers are never padded.
9
9
  */
@@ -13,7 +13,7 @@ export function formatMatchLine(
13
13
  isMatch: boolean,
14
14
  options: { useHashLines: boolean },
15
15
  ): string {
16
- const separator = isMatch ? HASHLINE_CONTENT_SEPARATOR : "-";
16
+ const separator = isMatch ? ">" : ":";
17
17
  if (options.useHashLines) {
18
18
  return `${lineNumber}${computeLineHash(lineNumber, line)}${separator}${line}`;
19
19
  }
package/src/tools/read.ts CHANGED
@@ -92,17 +92,13 @@ function prependLineNumbers(text: string, startNum: number): string {
92
92
  return textLines.map((line, i) => `${startNum + i}|${line}`).join("\n");
93
93
  }
94
94
 
95
- function prependHashLines(text: string, startNum: number): string {
96
- return formatHashLines(text, startNum);
97
- }
98
-
99
95
  function formatTextWithMode(
100
96
  text: string,
101
97
  startNum: number,
102
98
  shouldAddHashLines: boolean,
103
99
  shouldAddLineNumbers: boolean,
104
100
  ): string {
105
- if (shouldAddHashLines) return prependHashLines(text, startNum);
101
+ if (shouldAddHashLines) return formatHashLines(text, startNum);
106
102
  if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
107
103
  return text;
108
104
  }
@@ -59,7 +59,7 @@ export interface WriteToolDetails {
59
59
  /**
60
60
  * Strip hashline display prefixes from write content.
61
61
  *
62
- * Only active when hashline edit mode is enabled — the model sees `LINE+ID:`
62
+ * Only active when hashline edit mode is enabled — the model sees `LINE+ID|`
63
63
  * prefixes in read output and sometimes copies them into write content.
64
64
  */
65
65
  function stripWriteContent(session: ToolSession, content: string): { text: string; stripped: boolean } {
@@ -418,7 +418,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
418
418
  context?: AgentToolContext,
419
419
  ): Promise<AgentToolResult<WriteToolDetails>> {
420
420
  return untilAborted(signal, async () => {
421
- // Strip hashline display prefixes (LINE+ID:) if the model copied them from read output
421
+ // Strip hashline display prefixes (LINE+ID|) if the model copied them from read output
422
422
  const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
423
423
  const resolvedArchivePath = await this.#resolveArchiveWritePath(path);
424
424
  if (resolvedArchivePath) {