@oh-my-pi/pi-coding-agent 16.3.12 → 16.3.14

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli.js +3252 -3254
  3. package/dist/types/config/keybindings.d.ts +9 -4
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/extensibility/extensions/types.d.ts +11 -2
  6. package/dist/types/mnemopi/state.d.ts +7 -3
  7. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  8. package/dist/types/modes/components/read-tool-group.d.ts +1 -0
  9. package/dist/types/modes/interactive-mode.d.ts +3 -1
  10. package/dist/types/modes/rpc/rpc-client.d.ts +11 -5
  11. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  12. package/dist/types/modes/types.d.ts +3 -1
  13. package/dist/types/session/agent-session.d.ts +3 -4
  14. package/dist/types/tools/read.d.ts +1 -0
  15. package/dist/types/tools/renderers.d.ts +12 -5
  16. package/dist/types/tools/ssh.d.ts +4 -1
  17. package/dist/types/tools/write.d.ts +1 -0
  18. package/dist/types/utils/thinking-display.d.ts +5 -0
  19. package/package.json +12 -12
  20. package/src/config/keybindings.ts +62 -10
  21. package/src/config/model-registry.ts +85 -20
  22. package/src/config/settings.ts +48 -21
  23. package/src/extensibility/extensions/runner.ts +1 -0
  24. package/src/extensibility/extensions/types.ts +13 -2
  25. package/src/internal-urls/docs-index.generated.txt +1 -1
  26. package/src/mnemopi/state.ts +19 -5
  27. package/src/modes/acp/acp-agent.ts +69 -8
  28. package/src/modes/acp/acp-event-mapper.ts +1 -1
  29. package/src/modes/components/read-tool-group.ts +5 -1
  30. package/src/modes/components/tool-execution.ts +28 -24
  31. package/src/modes/components/transcript-container.ts +33 -0
  32. package/src/modes/controllers/extension-ui-controller.test.ts +16 -0
  33. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  34. package/src/modes/controllers/input-controller.ts +5 -55
  35. package/src/modes/interactive-mode.ts +43 -2
  36. package/src/modes/rpc/rpc-client.ts +42 -13
  37. package/src/modes/rpc/rpc-mode.ts +21 -19
  38. package/src/modes/types.ts +3 -0
  39. package/src/prompts/agents/plan.md +0 -1
  40. package/src/prompts/agents/reviewer.md +0 -1
  41. package/src/prompts/system/workflow-notice.md +5 -5
  42. package/src/prompts/tools/grep.md +2 -1
  43. package/src/prompts/tools/memory-edit.md +2 -0
  44. package/src/session/agent-session.ts +8 -5
  45. package/src/tools/grep.ts +58 -13
  46. package/src/tools/image-gen.ts +1 -1
  47. package/src/tools/memory-edit.ts +3 -1
  48. package/src/tools/read.ts +33 -13
  49. package/src/tools/renderers.ts +13 -5
  50. package/src/tools/ssh.ts +10 -3
  51. package/src/tools/tts.ts +1 -1
  52. package/src/tools/write.ts +26 -0
  53. package/src/utils/thinking-display.ts +74 -28
@@ -985,6 +985,24 @@ function countLines(text: string): number {
985
985
  return text.split("\n").length;
986
986
  }
987
987
 
988
+ /** Bounded newline scan: whether `text` spans more than `maxLines` lines.
989
+ * Runs on every live compose (the repaint predicate below), so it must not
990
+ * materialize the split the way `countLines` does. */
991
+ function exceedsLineCount(text: string, maxLines: number): boolean {
992
+ if (!text) return false;
993
+ let lines = 1;
994
+ for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
995
+ if (++lines > maxLines) return true;
996
+ }
997
+ return false;
998
+ }
999
+
1000
+ function writeContentOf(args: unknown): string {
1001
+ if (args == null || typeof args !== "object" || !("content" in args)) return "";
1002
+ const content = args.content;
1003
+ return typeof content === "string" ? content : "";
1004
+ }
1005
+
988
1006
  function formatLineCountSuffix(lineCount: number, uiTheme: Theme): string {
989
1007
  if (lineCount <= 0) return "";
990
1008
  return uiTheme.fg("dim", ` · ${lineCount} line${lineCount === 1 ? "" : "s"}`);
@@ -1218,4 +1236,12 @@ export const writeToolRenderer = {
1218
1236
  });
1219
1237
  },
1220
1238
  mergeCallAndResult: true,
1239
+ // The collapsed pending preview follows the streaming edge with a tail
1240
+ // window once the content outgrows it (`… (N earlier lines)` + last rows);
1241
+ // the first partial result re-anchors the frame to the top of the file, so
1242
+ // tail rows already committed to viewport/native scrollback would survive
1243
+ // as stale content above the new frame without a full replay. Expanded and
1244
+ // short previews stay top-anchored and skip the (scrollback-wiping) reset.
1245
+ forceFirstResultViewportRepaint: (args: unknown, options: RenderResultOptions) =>
1246
+ !options.expanded && exceedsLineCount(writeContentOf(args), WRITE_STREAMING_PREVIEW_LINES),
1221
1247
  };
@@ -1,14 +1,16 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
 
3
- // Single-entry memo for the proseOnly formatting path. During a streaming tick
4
- // the same growing thinking text is formatted up to three times (reveal count,
5
- // reveal slice, component render); this collapses them to one computation. The
6
- // `proseOnly === false` branch is a passthrough and never consults the cache, so
7
- // the key can be the text alone. A single entry is enough for the common case of
8
- // one active thinking block and never regresses (a miss recomputes exactly as
9
- // before).
10
- let formatCacheKey = "";
11
- let formatCacheValue = "";
3
+ // Single-slot-per-mode memo for formatThinkingForDisplay. During a streaming
4
+ // tick the same growing thinking text is formatted up to three times (reveal
5
+ // count, reveal slice, component render); this collapses them to one
6
+ // computation. Prose and raw modes produce different output for the same text,
7
+ // so each mode keeps its own slot. One entry per mode is enough for the common
8
+ // case of one active thinking block and never regresses (a miss recomputes
9
+ // exactly as before).
10
+ let proseCacheKey = "";
11
+ let proseCacheValue = "";
12
+ let rawCacheKey = "";
13
+ let rawCacheValue = "";
12
14
 
13
15
  export function canonicalizeMessage(text: string | null | undefined): string {
14
16
  if (!text) return "";
@@ -22,9 +24,35 @@ export function canonicalizeMessage(text: string | null | undefined): string {
22
24
  return "";
23
25
  }
24
26
 
27
+ // gpt-5.x reasoning summaries pad every summary part with an empty HTML
28
+ // comment (`**Headline**\n\n<!-- -->`), streamed as a `<!--` delta followed by
29
+ // ` -->`. Comments with actual content are left untouched.
30
+ const EMPTY_COMMENT_RE = /^<!--\s*-->$/;
31
+ const OPEN_COMMENT_RE = /^<!--\s*$/;
32
+
33
+ /**
34
+ * Whether `line` is reasoning-summary comment noise: an empty HTML comment,
35
+ * or its still-unterminated `<!--` prefix on the last line while streaming.
36
+ */
37
+ function isCommentNoise(line: string, isLastLine: boolean): boolean {
38
+ const trimmed = line.trim();
39
+ return EMPTY_COMMENT_RE.test(trimmed) || (isLastLine && OPEN_COMMENT_RE.test(trimmed));
40
+ }
41
+
42
+ /**
43
+ * Thinking text prepared for display. Both modes drop empty `<!-- -->`
44
+ * sentinel lines outside code fences (see {@link isCommentNoise}); prose-only
45
+ * mode additionally elides fenced code down to a trailing ellipsis.
46
+ */
25
47
  export function formatThinkingForDisplay(text: string, proseOnly: boolean): string {
26
- if (!proseOnly || !text) return text;
27
- if (text === formatCacheKey) return formatCacheValue;
48
+ if (!text) return text;
49
+ const hasComment = text.includes("<!--");
50
+ if (proseOnly) {
51
+ if (text === proseCacheKey) return proseCacheValue;
52
+ } else {
53
+ if (!hasComment) return text;
54
+ if (text === rawCacheKey) return rawCacheValue;
55
+ }
28
56
 
29
57
  const lines = text.split("\n");
30
58
  const resultLines: string[] = [];
@@ -56,22 +84,31 @@ export function formatThinkingForDisplay(text: string, proseOnly: boolean): stri
56
84
 
57
85
  for (let i = 0; i < lines.length; i++) {
58
86
  const line = lines[i]!;
59
- const open = FENCE.exec(line);
60
87
 
61
88
  if (inFence) {
89
+ const close = FENCE.exec(line);
62
90
  // A closing fence is the same char, at least as long, with nothing else on the line.
63
91
  if (
64
- open &&
65
- open[2]![0] === fenceChar &&
66
- open[2]!.length >= fenceLen &&
67
- line.slice(open[1]!.length + open[2]!.length).trim() === ""
92
+ close &&
93
+ close[2]![0] === fenceChar &&
94
+ close[2]!.length >= fenceLen &&
95
+ line.slice(close[1]!.length + close[2]!.length).trim() === ""
68
96
  ) {
69
97
  inFence = false;
70
98
  fenceChar = "";
71
99
  fenceLen = 0;
72
100
  }
73
- // We skip all internal lines of a code fence.
74
- } else if (open) {
101
+ // Prose mode skips all fence lines; raw mode keeps them verbatim
102
+ // (comment markers inside fences are code, not noise).
103
+ if (!proseOnly) resultLines.push(line);
104
+ continue;
105
+ }
106
+
107
+ // Drop the whole line so `**Headline**\n\n<!-- -->` leaves no blank tail.
108
+ if (hasComment && isCommentNoise(line, i === lines.length - 1)) continue;
109
+
110
+ const open = FENCE.exec(line);
111
+ if (open) {
75
112
  const marker = open[2]!;
76
113
  const ch = marker[0]!;
77
114
  // A backtick fence's info string may not contain a backtick.
@@ -79,18 +116,25 @@ export function formatThinkingForDisplay(text: string, proseOnly: boolean): stri
79
116
  inFence = true;
80
117
  fenceChar = ch;
81
118
  fenceLen = marker.length;
82
- appendEllipsis();
83
- } else {
84
- resultLines.push(line);
119
+ if (proseOnly) {
120
+ appendEllipsis();
121
+ } else {
122
+ resultLines.push(line);
123
+ }
124
+ continue;
85
125
  }
86
- } else {
87
- resultLines.push(line);
88
126
  }
127
+ resultLines.push(line);
89
128
  }
90
129
 
91
130
  const formatted = resultLines.join("\n");
92
- formatCacheKey = text;
93
- formatCacheValue = formatted;
131
+ if (proseOnly) {
132
+ proseCacheKey = text;
133
+ proseCacheValue = formatted;
134
+ } else {
135
+ rawCacheKey = text;
136
+ rawCacheValue = formatted;
137
+ }
94
138
  return formatted;
95
139
  }
96
140
 
@@ -99,9 +143,11 @@ export function hasDisplayableThinking(
99
143
  text: string | null | undefined,
100
144
  formattedText: string | null | undefined,
101
145
  ): boolean {
102
- if (!text) return false;
103
- if (!formattedText) return false;
104
- return formattedText.length > 0 && canonicalizeMessage(text).length > 0;
146
+ if (!text || !formattedText) return false;
147
+ // Visibility keys off the formatted text: a block whose raw text is only
148
+ // comment noise (`<!-- -->\n`) formats to whitespace and stays hidden. The
149
+ // raw canonicalize check still hides dot/ellipsis-only placeholder blocks.
150
+ return formattedText.trim().length > 0 && canonicalizeMessage(text).length > 0;
105
151
  }
106
152
 
107
153
  /** Whether an assistant message contains thinking content the TUI can reveal. */