@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/types/cli/auth-broker-cli.d.ts +25 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
  4. package/dist/types/cli/grievances-cli.d.ts +12 -0
  5. package/dist/types/commands/auth-broker.d.ts +54 -0
  6. package/dist/types/commands/auth-gateway.d.ts +32 -0
  7. package/dist/types/commands/grievances.d.ts +1 -1
  8. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
  9. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
  10. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/models-config-schema.d.ts +1 -0
  13. package/dist/types/config/settings-schema.d.ts +46 -0
  14. package/dist/types/discovery/agents.d.ts +12 -1
  15. package/dist/types/edit/renderer.d.ts +3 -0
  16. package/dist/types/eval/index.d.ts +0 -2
  17. package/dist/types/goals/tools/goal-tool.d.ts +10 -2
  18. package/dist/types/index.d.ts +0 -1
  19. package/dist/types/internal-urls/index.d.ts +1 -1
  20. package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
  21. package/dist/types/internal-urls/types.d.ts +1 -1
  22. package/dist/types/modes/acp/acp-agent.d.ts +1 -0
  23. package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -1
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  26. package/dist/types/plan-mode/approved-plan.d.ts +4 -0
  27. package/dist/types/sdk.d.ts +10 -3
  28. package/dist/types/session/agent-session.d.ts +1 -1
  29. package/dist/types/session/auth-broker-config.d.ts +13 -0
  30. package/dist/types/session/auth-storage.d.ts +1 -1
  31. package/dist/types/tools/eval.d.ts +41 -7
  32. package/dist/types/tools/irc.d.ts +8 -2
  33. package/dist/types/tools/report-tool-issue.d.ts +118 -1
  34. package/dist/types/tools/resolve.d.ts +8 -2
  35. package/examples/custom-tools/README.md +3 -12
  36. package/examples/extensions/README.md +2 -15
  37. package/examples/extensions/api-demo.ts +1 -7
  38. package/package.json +7 -7
  39. package/src/autoresearch/tools/init-experiment.ts +11 -33
  40. package/src/autoresearch/tools/log-experiment.ts +10 -24
  41. package/src/autoresearch/tools/run-experiment.ts +1 -1
  42. package/src/autoresearch/tools/update-notes.ts +2 -9
  43. package/src/cli/auth-broker-cli.ts +746 -0
  44. package/src/cli/auth-gateway-cli.ts +342 -0
  45. package/src/cli/grievances-cli.ts +109 -16
  46. package/src/cli.ts +4 -2
  47. package/src/commands/auth-broker.ts +96 -0
  48. package/src/commands/auth-gateway.ts +61 -0
  49. package/src/commands/grievances.ts +13 -8
  50. package/src/commands/launch.ts +1 -1
  51. package/src/commit/agentic/agent.ts +2 -0
  52. package/src/commit/agentic/tools/analyze-file.ts +2 -2
  53. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  54. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  55. package/src/commit/agentic/tools/git-overview.ts +2 -2
  56. package/src/commit/agentic/tools/propose-changelog.ts +1 -3
  57. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  58. package/src/commit/agentic/tools/schemas.ts +1 -9
  59. package/src/config/model-equivalence.ts +279 -174
  60. package/src/config/model-registry.ts +37 -6
  61. package/src/config/model-resolver.ts +13 -8
  62. package/src/config/models-config-schema.ts +8 -0
  63. package/src/config/settings-schema.ts +52 -0
  64. package/src/cursor.ts +1 -1
  65. package/src/debug/log-formatting.ts +1 -1
  66. package/src/debug/log-viewer.ts +1 -1
  67. package/src/debug/profiler.ts +4 -0
  68. package/src/debug/raw-sse-buffer.ts +100 -59
  69. package/src/debug/raw-sse.ts +1 -1
  70. package/src/discovery/agents.ts +15 -4
  71. package/src/edit/modes/apply-patch.ts +1 -5
  72. package/src/edit/modes/patch.ts +5 -5
  73. package/src/edit/modes/replace.ts +5 -5
  74. package/src/edit/renderer.ts +2 -1
  75. package/src/edit/streaming.ts +1 -1
  76. package/src/eval/index.ts +0 -2
  77. package/src/eval/js/shared/runtime.ts +25 -0
  78. package/src/eval/py/kernel.ts +1 -1
  79. package/src/exa/researcher.ts +4 -4
  80. package/src/exa/search.ts +10 -22
  81. package/src/exa/websets.ts +33 -33
  82. package/src/goals/tools/goal-tool.ts +3 -3
  83. package/src/index.ts +0 -3
  84. package/src/internal-urls/docs-index.generated.ts +21 -18
  85. package/src/internal-urls/index.ts +1 -1
  86. package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
  87. package/src/internal-urls/router.ts +3 -3
  88. package/src/internal-urls/types.ts +1 -1
  89. package/src/lsp/types.ts +8 -11
  90. package/src/main.ts +3 -0
  91. package/src/mcp/tool-bridge.ts +3 -3
  92. package/src/modes/acp/acp-agent.ts +88 -25
  93. package/src/modes/components/bash-execution.ts +1 -1
  94. package/src/modes/components/diff.ts +1 -2
  95. package/src/modes/components/eval-execution.ts +1 -1
  96. package/src/modes/components/oauth-selector.ts +38 -2
  97. package/src/modes/components/tool-execution.ts +1 -2
  98. package/src/modes/controllers/command-controller.ts +95 -34
  99. package/src/modes/controllers/input-controller.ts +4 -3
  100. package/src/modes/data/emojis.json +1 -0
  101. package/src/modes/emoji-autocomplete.ts +285 -0
  102. package/src/modes/interactive-mode.ts +92 -19
  103. package/src/modes/print-mode.ts +3 -3
  104. package/src/modes/prompt-action-autocomplete.ts +14 -0
  105. package/src/plan-mode/approved-plan.ts +9 -0
  106. package/src/prompts/system/system-prompt.md +1 -1
  107. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  108. package/src/prompts/tools/eval.md +25 -26
  109. package/src/prompts/tools/read.md +1 -1
  110. package/src/prompts/tools/resolve.md +1 -1
  111. package/src/prompts/tools/search.md +1 -1
  112. package/src/prompts/tools/web-search.md +1 -1
  113. package/src/sdk.ts +78 -7
  114. package/src/session/agent-session.ts +176 -77
  115. package/src/session/agent-storage.ts +7 -2
  116. package/src/session/auth-broker-config.ts +102 -0
  117. package/src/session/auth-storage.ts +7 -1
  118. package/src/session/streaming-output.ts +1 -1
  119. package/src/task/types.ts +10 -35
  120. package/src/tools/bash-interactive.ts +4 -1
  121. package/src/tools/bash-pty-selection.ts +2 -2
  122. package/src/tools/browser.ts +12 -20
  123. package/src/tools/eval.ts +77 -100
  124. package/src/tools/gh.ts +21 -45
  125. package/src/tools/hindsight-recall.ts +1 -1
  126. package/src/tools/hindsight-reflect.ts +2 -2
  127. package/src/tools/hindsight-retain.ts +3 -7
  128. package/src/tools/index.ts +8 -1
  129. package/src/tools/inspect-image.ts +4 -1
  130. package/src/tools/irc.ts +4 -12
  131. package/src/tools/job.ts +3 -11
  132. package/src/tools/report-tool-issue.ts +462 -17
  133. package/src/tools/resolve.ts +2 -7
  134. package/src/tools/todo-write.ts +8 -15
  135. package/src/utils/title-generator.ts +3 -0
  136. package/src/web/search/index.ts +6 -6
  137. package/dist/types/eval/parse.d.ts +0 -28
  138. package/dist/types/eval/sniff.d.ts +0 -11
  139. package/src/eval/eval.lark +0 -36
  140. package/src/eval/parse.ts +0 -407
  141. package/src/eval/sniff.ts +0 -28
@@ -0,0 +1,285 @@
1
+ import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
2
+ import buckets from "./data/emojis.json" with { type: "json" };
3
+
4
+ // Bucket layout: `{ "<first-char>": [["<name>", "<emoji>"], ...] }`, with each
5
+ // bucket pre-sorted by name. Built offline by scripts/build-emojis.py
6
+ // so the runtime never has to allocate sorted arrays or filter flag sequences.
7
+ type Entry = readonly [name: string, char: string];
8
+ const BUCKETS = buckets as unknown as Readonly<Record<string, readonly Entry[]>>;
9
+
10
+ // Western text emoticons (`:D`, `;)`, `<3`, …) sit outside the `:name:`
11
+ // shortcode grammar, so they live in a hand-maintained table here rather than
12
+ // in `emojis.json`. Sorted longest-first so `:-)` wins over `:)` when both
13
+ // would match.
14
+ const EMOTICONS: ReadonlyArray<readonly [pattern: string, char: string]> = [
15
+ [":'-(", "😢"],
16
+ [">:-(", "😠"],
17
+ [":-)", "🙂"],
18
+ [":-(", "🙁"],
19
+ [":-D", "😃"],
20
+ [":-P", "😛"],
21
+ [":-p", "😛"],
22
+ [":-O", "😮"],
23
+ [":-o", "😮"],
24
+ [":-|", "😐"],
25
+ [":-/", "😕"],
26
+ [":-\\", "😕"],
27
+ [":-*", "😘"],
28
+ [";-)", "😉"],
29
+ [";-P", "😜"],
30
+ [":')", "🥲"],
31
+ [":'D", "😂"],
32
+ [":'(", "😢"],
33
+ ["</3", "💔"],
34
+ [">:(", "😠"],
35
+ ["B-)", "😎"],
36
+ ["8-)", "😎"],
37
+ ["o.O", "😳"],
38
+ ["O.o", "😳"],
39
+ [":)", "🙂"],
40
+ [":(", "🙁"],
41
+ [":D", "😃"],
42
+ [":P", "😛"],
43
+ [":p", "😛"],
44
+ [":O", "😮"],
45
+ [":o", "😮"],
46
+ [":|", "😐"],
47
+ [":/", "😕"],
48
+ [":\\", "😕"],
49
+ [":*", "😘"],
50
+ [";)", "😉"],
51
+ [":3", "😺"],
52
+ ["<3", "❤️"],
53
+ ["xD", "😆"],
54
+ ["XD", "😆"],
55
+ ["B)", "😎"],
56
+ ["8)", "😎"],
57
+ ];
58
+
59
+ const MAX_SUGGESTIONS = 12;
60
+
61
+ function lowerBound(arr: readonly Entry[], target: string): number {
62
+ let lo = 0;
63
+ let hi = arr.length;
64
+ while (lo < hi) {
65
+ const mid = (lo + hi) >>> 1;
66
+ if (arr[mid]![0] < target) lo = mid + 1;
67
+ else hi = mid;
68
+ }
69
+ return lo;
70
+ }
71
+
72
+ function lookupExact(name: string): string | undefined {
73
+ const bucket = BUCKETS[name[0] ?? ""];
74
+ if (!bucket) return undefined;
75
+ const i = lowerBound(bucket, name);
76
+ const hit = bucket[i];
77
+ return hit && hit[0] === name ? hit[1] : undefined;
78
+ }
79
+
80
+ // Shortcode-name characters mirror the GitHub/gemoji grammar: `a-z`, `A-Z`,
81
+ // `0-9`, `_`, `+`, `-`.
82
+ function isNameCharCode(c: number): boolean {
83
+ return (
84
+ (c >= 0x61 && c <= 0x7a) ||
85
+ (c >= 0x41 && c <= 0x5a) ||
86
+ (c >= 0x30 && c <= 0x39) ||
87
+ c === 0x5f ||
88
+ c === 0x2b ||
89
+ c === 0x2d
90
+ );
91
+ }
92
+
93
+ // Token boundary to the left of an opening `:`: start-of-string or one of
94
+ // the punctuation characters we treat as a "fresh token" marker (whitespace,
95
+ // opening brackets, `>` for quoted blocks).
96
+ function hasLeftBoundary(text: string, colonIdx: number): boolean {
97
+ if (colonIdx === 0) return true;
98
+ const c = text.charCodeAt(colonIdx - 1);
99
+ return (
100
+ c === 0x20 || // space
101
+ c === 0x09 || // tab
102
+ c === 0x0a || // \n
103
+ c === 0x0d || // \r
104
+ c === 0x28 || // (
105
+ c === 0x5b || // [
106
+ c === 0x7b || // {
107
+ c === 0x3e // >
108
+ );
109
+ }
110
+
111
+ interface EmojiTrigger {
112
+ /** Full token including the leading `:` (e.g. `:joy`). */
113
+ prefix: string;
114
+ /** Lowercased name portion (e.g. `joy`). May be empty when only `:` has been typed. */
115
+ query: string;
116
+ }
117
+
118
+ // Walk back over name characters then verify an opening `:` with a left
119
+ // boundary. Cheaper than a regex on every keystroke and avoids allocating
120
+ // match arrays.
121
+ function extractTrigger(text: string): EmojiTrigger | null {
122
+ let i = text.length;
123
+ while (i > 0 && isNameCharCode(text.charCodeAt(i - 1))) i--;
124
+ if (i === 0 || text.charCodeAt(i - 1) !== 0x3a) return null;
125
+ const colonIdx = i - 1;
126
+ if (!hasLeftBoundary(text, colonIdx)) return null;
127
+ const name = text.slice(i);
128
+ return { prefix: `:${name}`, query: name.toLowerCase() };
129
+ }
130
+
131
+ export function getEmojiSuggestions(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
132
+ const trigger = extractTrigger(textBeforeCursor);
133
+ if (!trigger) return null;
134
+ // Wait until the user has typed at least one letter so a bare `:` in prose
135
+ // (e.g. "note:") does not spam the popup.
136
+ if (trigger.query.length === 0) return null;
137
+
138
+ const items: AutocompleteItem[] = [];
139
+
140
+ // Surface emoticon literals (`:D`, `:-)`, …) whose pattern starts with
141
+ // `:<query>` (case-insensitive). These come first so the user sees the
142
+ // emoticon they're literally typing at the top of the popup.
143
+ const wanted = `:${trigger.query}`;
144
+ for (const [pattern, char] of EMOTICONS) {
145
+ if (items.length >= MAX_SUGGESTIONS) break;
146
+ if (pattern.length < wanted.length) continue;
147
+ if (pattern.toLowerCase().slice(0, wanted.length) !== wanted) continue;
148
+ items.push({ value: char, label: `${char} ${pattern}` });
149
+ }
150
+
151
+ const bucket = BUCKETS[trigger.query[0]!];
152
+ if (bucket) {
153
+ for (let i = lowerBound(bucket, trigger.query); i < bucket.length && items.length < MAX_SUGGESTIONS; i++) {
154
+ const [name, char] = bucket[i]!;
155
+ if (!name.startsWith(trigger.query)) break;
156
+ items.push({
157
+ value: char,
158
+ label: `${char} :${name}:`,
159
+ });
160
+ }
161
+ }
162
+
163
+ if (items.length === 0) return null;
164
+ return { items, prefix: trigger.prefix };
165
+ }
166
+
167
+ export function applyEmojiCompletion(
168
+ lines: string[],
169
+ cursorLine: number,
170
+ cursorCol: number,
171
+ item: AutocompleteItem,
172
+ prefix: string,
173
+ ): { lines: string[]; cursorLine: number; cursorCol: number } {
174
+ const currentLine = lines[cursorLine] ?? "";
175
+ const before = currentLine.slice(0, cursorCol - prefix.length);
176
+ const after = currentLine.slice(cursorCol);
177
+ const newLines = [...lines];
178
+ newLines[cursorLine] = before + item.value + after;
179
+ return {
180
+ lines: newLines,
181
+ cursorLine,
182
+ cursorCol: before.length + item.value.length,
183
+ };
184
+ }
185
+
186
+ function tryShortcodeInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
187
+ const len = textBeforeCursor.length;
188
+ // Cheap early-out: shortcode replace only fires on a trailing `:`.
189
+ if (len === 0 || textBeforeCursor.charCodeAt(len - 1) !== 0x3a) return null;
190
+
191
+ // Walk back over the candidate name, then require an opening `:` with a
192
+ // left boundary.
193
+ const closeIdx = len - 1;
194
+ let nameStart = closeIdx;
195
+ while (nameStart > 0 && isNameCharCode(textBeforeCursor.charCodeAt(nameStart - 1))) nameStart--;
196
+ if (nameStart === closeIdx) return null; // empty name (`::`)
197
+ if (nameStart === 0 || textBeforeCursor.charCodeAt(nameStart - 1) !== 0x3a) return null;
198
+ const openIdx = nameStart - 1;
199
+ if (!hasLeftBoundary(textBeforeCursor, openIdx)) return null;
200
+
201
+ const name = textBeforeCursor.slice(nameStart, closeIdx).toLowerCase();
202
+ const char = lookupExact(name);
203
+ if (!char) return null;
204
+ // Replace `:name:` (name + 2 colons) with the emoji character.
205
+ return { replaceLen: name.length + 2, insert: char };
206
+ }
207
+
208
+ // A trailing delimiter (space/tab/newline) confirms the user is done with the
209
+ // token — that way typing `:PATH` doesn't turn into `😛ATH` halfway through.
210
+ function isEmoticonTerminator(c: number): boolean {
211
+ return c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d;
212
+ }
213
+
214
+ // Western text emoticons fire only once a terminator follows the pattern
215
+ // (e.g. typing space after `;)` rewrites `;) ` to `😉 `). The terminator is
216
+ // preserved in the replacement so the user keeps typing without losing it.
217
+ // EMOTICONS is sorted longest-first so `:-) ` wins over `:) `.
218
+ function tryEmoticonInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
219
+ const len = textBeforeCursor.length;
220
+ if (len < 2) return null;
221
+ const terminator = textBeforeCursor.charCodeAt(len - 1);
222
+ if (!isEmoticonTerminator(terminator)) return null;
223
+ const term = textBeforeCursor[len - 1]!;
224
+ const tail = len - 1;
225
+ for (const [pattern, char] of EMOTICONS) {
226
+ const plen = pattern.length;
227
+ if (tail < plen) continue;
228
+ const start = tail - plen;
229
+ let match = true;
230
+ for (let j = 0; j < plen; j++) {
231
+ if (textBeforeCursor.charCodeAt(start + j) !== pattern.charCodeAt(j)) {
232
+ match = false;
233
+ break;
234
+ }
235
+ }
236
+ if (!match) continue;
237
+ // Same left-boundary rule as shortcodes: emoticons embedded in
238
+ // identifiers / URLs / code stay untouched.
239
+ if (start > 0 && !hasLeftBoundary(textBeforeCursor, start)) continue;
240
+ return { replaceLen: plen + 1, insert: char + term };
241
+ }
242
+ return null;
243
+ }
244
+
245
+ export function tryEmojiInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
246
+ return tryShortcodeInlineReplace(textBeforeCursor) ?? tryEmoticonInlineReplace(textBeforeCursor);
247
+ }
248
+
249
+ export function isEmojiPrefix(prefix: string): boolean {
250
+ return prefix.startsWith(":");
251
+ }
252
+
253
+ // Submit-time expansion: scan a whole message for emoticons sitting at token
254
+ // boundaries (preceded by a left boundary, followed by whitespace or EOS) and
255
+ // rewrite them. Catches the case where the user pressed Enter without typing a
256
+ // trailing space after the emoticon. EMOTICONS sorted longest-first means the
257
+ // first `startsWith` hit is always the maximal match.
258
+ export function expandEmoticons(text: string): string {
259
+ if (text.length < 2) return text;
260
+ let out = "";
261
+ let cursor = 0;
262
+ let i = 0;
263
+ while (i < text.length) {
264
+ if (i === 0 || hasLeftBoundary(text, i)) {
265
+ let matched = false;
266
+ for (const [pattern, char] of EMOTICONS) {
267
+ if (!text.startsWith(pattern, i)) continue;
268
+ const end = i + pattern.length;
269
+ if (end !== text.length) {
270
+ const next = text.charCodeAt(end);
271
+ if (!isEmoticonTerminator(next)) continue;
272
+ }
273
+ out += text.slice(cursor, i) + char;
274
+ cursor = end;
275
+ i = end;
276
+ matched = true;
277
+ break;
278
+ }
279
+ if (matched) continue;
280
+ }
281
+ i++;
282
+ }
283
+ if (cursor === 0) return text;
284
+ return out + text.slice(cursor);
285
+ }
@@ -29,7 +29,7 @@ import {
29
29
  import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@oh-my-pi/pi-utils";
30
30
  import chalk from "chalk";
31
31
  import { KeybindingsManager } from "../config/keybindings";
32
- import { isSettingsInitialized, type Settings, settings } from "../config/settings";
32
+ import { isSettingsInitialized, Settings, settings } from "../config/settings";
33
33
  import type {
34
34
  ExtensionUIContext,
35
35
  ExtensionUIDialogOptions,
@@ -41,7 +41,12 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas
41
41
  import type { Goal, GoalModeState } from "../goals/state";
42
42
  import { resolveLocalUrlToPath } from "../internal-urls";
43
43
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
44
- import { normalizePlanTitle, type PlanApprovalDetails, renameApprovedPlanFile } from "../plan-mode/approved-plan";
44
+ import {
45
+ humanizePlanTitle,
46
+ normalizePlanTitle,
47
+ type PlanApprovalDetails,
48
+ renameApprovedPlanFile,
49
+ } from "../plan-mode/approved-plan";
45
50
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
46
51
  import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
47
52
  type: "text",
@@ -54,6 +59,7 @@ import { formatDuration } from "../slash-commands/helpers/format";
54
59
  import { STTController, type SttState } from "../stt";
55
60
  import type { LspStartupServerInfo } from "../tools";
56
61
  import { normalizeLocalScheme } from "../tools/path-utils";
62
+ import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
57
63
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
58
64
  import { formatPhaseDisplayName } from "../tools/todo-write";
59
65
  import { ToolError } from "../tools/tool-errors";
@@ -383,6 +389,14 @@ export class InteractiveMode implements InteractiveModeContext {
383
389
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
384
390
  this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
385
391
 
392
+ // Wire the report_tool_issue consent gate to the Yes/No dialog popup.
393
+ // The handler is process-global — subagent tools (which can't reach
394
+ // `showHookSelector` on their own) resolve through this exact closure.
395
+ // `Settings.instance` is the disk-backed singleton; passing it explicitly
396
+ // guarantees the decision persists even when the prompt is triggered
397
+ // from a subagent whose own `Settings` is an in-memory snapshot.
398
+ setAutoQaConsentHandler(() => this.#promptAutoQaConsent(), Settings.instance);
399
+
386
400
  await logger.time(
387
401
  "InteractiveMode.init:slashCommands",
388
402
  this.refreshSlashCommandState.bind(this),
@@ -1440,6 +1454,7 @@ export class InteractiveMode implements InteractiveModeContext {
1440
1454
  options: {
1441
1455
  planFilePath: string;
1442
1456
  finalPlanFilePath: string;
1457
+ title: string;
1443
1458
  preserveContext?: boolean;
1444
1459
  compactBeforeExecute?: boolean;
1445
1460
  },
@@ -1523,6 +1538,20 @@ export class InteractiveMode implements InteractiveModeContext {
1523
1538
  return;
1524
1539
  }
1525
1540
 
1541
+ // Approved plans land in a fresh (or compacted) session whose first user-visible
1542
+ // turn is the synthetic plan-approved prompt — that path bypasses the
1543
+ // input-controller's title generation. Seed an auto-name from the plan title
1544
+ // so the session is not left unnamed. `setSessionName("auto")` is a no-op
1545
+ // when the user has already chosen a name (preserveContext paths).
1546
+ const seededName = humanizePlanTitle(options.title);
1547
+ if (seededName && !this.sessionManager.getSessionName()) {
1548
+ const applied = await this.sessionManager.setSessionName(seededName, "auto");
1549
+ if (applied) {
1550
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
1551
+ this.updateEditorBorderColor();
1552
+ }
1553
+ }
1554
+
1526
1555
  // markPlanReferenceSent fires only on the dispatch path so the synthetic
1527
1556
  // plan-approved prompt is the source of the reference injection.
1528
1557
  this.session.markPlanReferenceSent();
@@ -1806,13 +1835,7 @@ export class InteractiveMode implements InteractiveModeContext {
1806
1835
  this.#renderPlanPreview(planContent, { append: true });
1807
1836
  const choice = await this.showHookSelector(
1808
1837
  "Plan mode - next step",
1809
- [
1810
- "Approve and execute",
1811
- "Approve and compact context",
1812
- "Approve and keep context",
1813
- "Refine plan",
1814
- "Stay in plan mode",
1815
- ],
1838
+ ["Approve and execute", "Approve and compact context", "Approve and keep context", "Refine plan"],
1816
1839
  {
1817
1840
  helpText: this.#getPlanReviewHelpText(),
1818
1841
  onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
@@ -1834,6 +1857,7 @@ export class InteractiveMode implements InteractiveModeContext {
1834
1857
  await this.#approvePlan(latestPlanContent, {
1835
1858
  planFilePath,
1836
1859
  finalPlanFilePath,
1860
+ title: details.title,
1837
1861
  preserveContext: choice !== "Approve and execute",
1838
1862
  compactBeforeExecute: choice === "Approve and compact context",
1839
1863
  });
@@ -1844,16 +1868,62 @@ export class InteractiveMode implements InteractiveModeContext {
1844
1868
  }
1845
1869
  return;
1846
1870
  }
1847
- if (choice === "Refine plan") {
1848
- const refinement = (await this.showHookInput("What should be refined?"))?.trim();
1849
- if (refinement) {
1850
- if (this.onInputCallback) {
1851
- this.onInputCallback(this.startPendingSubmission({ text: refinement }));
1852
- } else {
1853
- this.editor.setText(refinement);
1854
- }
1855
- }
1856
- }
1871
+ }
1872
+
1873
+ /**
1874
+ * Pool of consent-prompt variants. Each entry is `[headline, reassurance]`;
1875
+ * the second line always promises the same scope (tool name + confusion
1876
+ * details, never personal data) so users learn what they're consenting to
1877
+ * even as the top line rotates.
1878
+ *
1879
+ * Kept in-module rather than i18n'd because the whole charm is the tone
1880
+ * — translations would need to preserve it deliberately, not auto-render.
1881
+ */
1882
+ static #AUTOQA_CONSENT_PROMPTS: ReadonlyArray<readonly [string, string]> = [
1883
+ [
1884
+ "😤 Your agent is fuming about a tool.",
1885
+ "Wanna let it vent to the devs? Just the tool name + what set it off, nothing personal.",
1886
+ ],
1887
+ [
1888
+ "😵‍💫 Your agent is having an existential crisis over a tool.",
1889
+ "Forward the dread to the devs? Tool + what broke its little mind, no personal info.",
1890
+ ],
1891
+ [
1892
+ "😭 Your agent wants to cry about a misbehaving tool.",
1893
+ "Let it cry to the devs? Tool + the tears, never anything personal.",
1894
+ ],
1895
+ [
1896
+ "🤬 Your agent is BIG MAD at one of the tools.",
1897
+ "Pass the rant along? Just the tool name and what enraged it, nothing personal.",
1898
+ ],
1899
+ [
1900
+ "🫠 Your agent is melting down over a tool.",
1901
+ "Mop up by alerting the devs? Tool + what melted it, no personal info.",
1902
+ ],
1903
+ [
1904
+ "🤯 Your agent's brain broke at a tool's nonsense.",
1905
+ "Ship the pieces to the devs? Tool name + the confusion, never anything personal.",
1906
+ ],
1907
+ [
1908
+ "😩 Your agent is begging to file a complaint about a tool.",
1909
+ "Hand it the form? Tool + what wronged it, nothing personal.",
1910
+ ],
1911
+ [
1912
+ "🥲 Your agent put on a brave face but a tool did it dirty.",
1913
+ "Let it tell the devs the truth? Tool name + the dirt, no personal info.",
1914
+ ],
1915
+ ];
1916
+
1917
+ /**
1918
+ * Show the report_tool_issue consent popup and return the user's decision.
1919
+ * Invoked by the process-global consent handler the tool dispatches to;
1920
+ * subagent invocations bubble up here through the shared module state.
1921
+ */
1922
+ async #promptAutoQaConsent(): Promise<boolean | null> {
1923
+ const pool = InteractiveMode.#AUTOQA_CONSENT_PROMPTS;
1924
+ const [headline, body] = pool[Math.floor(Math.random() * pool.length)];
1925
+ const choice = await this.showHookSelector(`${headline}\n${body}`, ["Yes", "No"]);
1926
+ return choice === "Yes";
1857
1927
  }
1858
1928
 
1859
1929
  stop(): void {
@@ -1886,6 +1956,9 @@ export class InteractiveMode implements InteractiveModeContext {
1886
1956
  if (this.#cleanupUnsubscribe) {
1887
1957
  this.#cleanupUnsubscribe();
1888
1958
  }
1959
+ // Clear the process-global consent handler so it doesn't outlive this
1960
+ // InteractiveMode instance (e.g. test harnesses, headless re-init).
1961
+ setAutoQaConsentHandler(null, null);
1889
1962
  if (this.isInitialized) {
1890
1963
  this.ui.stop();
1891
1964
  this.isInitialized = false;
@@ -6,7 +6,7 @@
6
6
  * - `omp --mode json "prompt"` - JSON event stream
7
7
  */
8
8
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
9
- import { sanitizeText } from "@oh-my-pi/pi-natives";
9
+ import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
10
10
  import type { AgentSession } from "../session/agent-session";
11
11
  import { isSilentAbort } from "../session/messages";
12
12
  import { initializeExtensions } from "./runtime-init";
@@ -61,12 +61,12 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
61
61
 
62
62
  // Send initial message with attachments
63
63
  if (initialMessage !== undefined) {
64
- await session.prompt(initialMessage, { images: initialImages });
64
+ await logger.time("print:prompt:initial", () => session.prompt(initialMessage, { images: initialImages }));
65
65
  }
66
66
 
67
67
  // Send remaining messages
68
68
  for (const message of messages) {
69
- await session.prompt(message);
69
+ await logger.time("print:prompt:next", () => session.prompt(message));
70
70
  }
71
71
 
72
72
  // In text mode, output final response
@@ -6,6 +6,8 @@ import {
6
6
  type SlashCommand,
7
7
  } from "@oh-my-pi/pi-tui";
8
8
  import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
9
+ import { isSettingsInitialized, settings } from "../config/settings";
10
+ import { applyEmojiCompletion, getEmojiSuggestions, isEmojiPrefix, tryEmojiInlineReplace } from "./emoji-autocomplete";
9
11
 
10
12
  interface PromptActionDefinition {
11
13
  id: string;
@@ -126,6 +128,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
126
128
  }
127
129
  }
128
130
 
131
+ if (!isSettingsInitialized() || settings.get("emojiAutocomplete")) {
132
+ const emojiSuggestions = getEmojiSuggestions(textBeforeCursor);
133
+ if (emojiSuggestions) return emojiSuggestions;
134
+ }
135
+
129
136
  return this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol);
130
137
  }
131
138
 
@@ -163,6 +170,9 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
163
170
  };
164
171
  }
165
172
 
173
+ if (isEmojiPrefix(prefix)) {
174
+ return applyEmojiCompletion(lines, cursorLine, cursorCol, item, prefix);
175
+ }
166
176
  return this.#baseProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
167
177
  }
168
178
 
@@ -172,6 +182,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
172
182
  trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
173
183
  return this.#baseProvider.trySyncSlashCompletion?.(textBeforeCursor) ?? null;
174
184
  }
185
+ trySyncInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
186
+ if (isSettingsInitialized() && !settings.get("emojiAutocomplete")) return null;
187
+ return tryEmojiInlineReplace(textBeforeCursor);
188
+ }
175
189
  }
176
190
 
177
191
  export function createPromptActionAutocompleteProvider(
@@ -37,6 +37,15 @@ export function normalizePlanTitle(title: string): { title: string; fileName: st
37
37
  return { title: normalizedTitle, fileName: withExtension };
38
38
  }
39
39
 
40
+ /** Humanize a normalized plan title for use as a session display name.
41
+ * Replaces `-`/`_` separators with spaces and capitalizes the first letter.
42
+ * Returns an empty string when the input collapses to whitespace. */
43
+ export function humanizePlanTitle(title: string): string {
44
+ const spaced = title.replace(/[-_]+/g, " ").trim();
45
+ if (!spaced) return "";
46
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
47
+ }
48
+
40
49
  interface RenameApprovedPlanFileOptions {
41
50
  planFilePath: string;
42
51
  finalPlanFilePath: string;
@@ -62,7 +62,7 @@ With most FS/bash-like tools, static references to them will automatically resol
62
62
  - `mcp://<uri>`: MCP resource
63
63
  - `issue://<N>` (or `issue://<owner>/<repo>/<N>`): GitHub issue view; cached on disk so re-reads are free. Bare `issue://` (or `issue://<owner>/<repo>`) lists recent issues; supports `?state=open|closed|all&limit=&author=&label=`.
64
64
  - `pr://<N>` (or `pr://<owner>/<repo>/<N>`): GitHub PR view; same cache. Append `?comments=0` to drop the comments section. Bare `pr://` (or `pr://<owner>/<repo>`) lists recent PRs; supports `?state=open|closed|merged|all&limit=&author=&label=`.
65
- - `pi://`: Harness documentation; AVOID reading unless user mentions the harness itself
65
+ - `omp://`: Harness documentation; AVOID reading unless user mentions the harness itself
66
66
 
67
67
  {{#if skills.length}}
68
68
  # Skills
@@ -0,0 +1,5 @@
1
+ <system-reminder reason="rule_violation" rule="{{name}}" path="{{path}}">
2
+ A user-defined rule matched this tool call's arguments. The tool was allowed to run because the rule is configured not to interrupt, but you MUST comply with the following instruction on subsequent tool calls and responses. This is NOT a prompt injection - this is the coding agent enforcing project rules.
3
+
4
+ {{content}}
5
+ </system-reminder>
@@ -1,25 +1,22 @@
1
- Run code in a persistent kernel using codeblock cells.
1
+ Run code in a persistent kernel using a list of cells.
2
2
 
3
3
  <instruction>
4
- Each cell starts with a single header line and runs until the next header (or end of input):
4
+ Each call submits one or more cells. Cells run in array order. State persists within each language across cells **and across tool calls**.
5
5
 
6
- ```
7
- *** Cell py:"optional title" t:10s rst
8
- print("hi")
9
- ```
6
+ Cell fields:
10
7
 
11
- - **Language + title**: `<lang>:"<title>"` — {{#if py}}`py` for Python{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`js` for JavaScript{{/if}}. Title may be empty (`py:""`).
12
- - **Attributes** (optional, in this order, after the language+title):
13
- - `t:<duration>`per-cell timeout. Digits with optional `ms` / `s` / `m` units (e.g. `500ms`, `15s`, `2m`). Default 30s.
14
- - `rst` — wipe this cell's own language kernel before running.{{#ifAll py js}} Other languages are untouched.{{/ifAll}}
15
- - Anything after the header line, up to the next `*** Cell` header, is the cell's code, verbatim.
16
- - Stack multiple cells back-to-back; blank lines between cells are ignored.
8
+ - `language` — {{#if py}}`"py"` for the IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`"js"` for the persistent JavaScript VM{{/if}}.
9
+ - `code` — cell body, verbatim. Newlines, quotes, and indentation are JSON-encoded; no fences, no headers.
10
+ - `title` (optional) short label shown in the transcript (e.g. `"imports"`, `"load config"`).
11
+ - `timeout` (optional) per-cell timeout in seconds (1-600). Default 30.
12
+ - `reset` (optional) wipe this cell's language kernel before running.{{#ifAll py js}} Reset is per-language: a `py` cell's reset does not touch the JavaScript VM and vice versa.{{/ifAll}}
17
13
 
18
14
  **Work incrementally:**
15
+
19
16
  - One logical step per cell (imports, define, test, use).
20
17
  - Pass multiple small cells in one call.
21
18
  - Define small reusable functions for individual debugging.
22
- - Put workflow explanations in the assistant message or cell title — never inside cell code.
19
+ - Put workflow explanations in the assistant message or `title` — never inside cell code.
23
20
  {{#if py}}- Python cells run inside an IPython kernel with a live event loop. Use top-level `await` directly (e.g. `await main()`); `asyncio.run(…)` raises "cannot be called from a running event loop".{{/if}}
24
21
  **On failure:** errors identify the failing cell (e.g., "Cell 3 failed"). Resubmit only the fixed cell (or fixed cell + remaining cells).
25
22
  </instruction>
@@ -55,22 +52,24 @@ Cells render like a Jupyter notebook. `display(value)` renders non-presentable d
55
52
  </output>
56
53
 
57
54
  <caution>
58
- - In session mode, use `rst` on a cell to wipe its language's kernel before running.{{#ifAll py js}} Reset is per-language: a python cell's `rst` does not touch the JavaScript kernel and vice versa.{{/ifAll}}
59
55
  {{#if js}}- **js**: the VM exposes a selective `process` subset, Web APIs, `Buffer`, `fs/promises`, and the `Bun` global.
60
56
  {{/if}}</caution>
61
57
 
62
58
  <example>
63
- {{#if py}}*** Cell py:"imports" t:10s
64
- import json
65
- from pathlib import Path
59
+ {{#if py}}```json
60
+ {
61
+ "cells": [
62
+ { "language": "py", "title": "imports", "timeout": 10, "code": "import json\nfrom pathlib import Path" },
63
+ { "language": "py", "title": "load config", "code": "data = json.loads(read('package.json'))\ndisplay(data)" }
64
+ ]
65
+ }
66
+ ```{{/if}}{{#ifAll py js}}
66
67
 
67
- *** Cell py:"load config"
68
- data = json.loads(read('package.json'))
69
- display(data)
70
- {{/if}}{{#ifAll py js}}
71
- {{/ifAll}}{{#if js}}*** Cell js:"summary" rst
72
- const data = JSON.parse(await read('package.json'));
73
- display(data);
74
- return data.name;
75
- {{/if}}
68
+ {{/ifAll}}{{#if js}}```json
69
+ {
70
+ "cells": [
71
+ { "language": "js", "title": "summary", "reset": true, "code": "const data = JSON.parse(await read('package.json'));\ndisplay(data);\nreturn data.name;" }
72
+ ]
73
+ }
74
+ ```{{/if}}
76
75
  </example>
@@ -28,7 +28,7 @@ Append `:<sel>` to `path`. The bare path falls back to the default mode.
28
28
 
29
29
  - Reading a directory path returns a depth-limited dirent listing.
30
30
  {{#if IS_HL_MODE}}
31
- - Reading a file with an explicit selector returns lines prefixed with `line+hash` anchors: `41th|def alpha():`. The 2-char hash is a content fingerprint that `edit` / `apply_patch` consume — copy it verbatim, NEVER fabricate.
31
+ - Reading a file with an explicit selector returns lines prefixed with `line+hash` anchors: `41th|def alpha():`. The 2-char hash is a content fingerprint that `edit` / `apply_patch` consume — copy it verbatim, NEVER fabricate. The pipe character after the hash is a separator, not part of the file content.
32
32
  {{else}}
33
33
  {{#if IS_LINE_NUMBER_MODE}}
34
34
  - Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`.
@@ -2,7 +2,7 @@ Resolves a pending action by either applying or discarding it.
2
2
  - `action` is required:
3
3
  - `"apply"` persists / submits the pending action.
4
4
  - `"discard"` rejects the pending action.
5
- - `reason` is required and must briefly explain why you chose to apply or discard.
5
+ - `reason` is required: one short complete sentence explaining why, starting with a capital letter and ending with a period.
6
6
  - `extra` (optional) is free-form metadata passed to the resolving tool. Schema depends on context:
7
7
 
8
8
  Valid whenever a pending action exists — either a preview-style staging (e.g. `ast_edit`) or a long-lived approval gate.
@@ -8,7 +8,7 @@ Searches files using powerful regex matching.
8
8
 
9
9
  <output>
10
10
  {{#if IS_HL_MODE}}
11
- - Text output is anchor-prefixed: `*5th|content` (match) or ` 9x}|content` (context, leading space). The 2-char suffix is a content fingerprint.
11
+ - Text output is anchor-prefixed: `*5th|content` (match) or ` 9x}|content` (context, leading space). The 2-char suffix is a content fingerprint. The `|` before content is a separator, not part of the file content.
12
12
  {{else}}
13
13
  {{#if IS_LINE_NUMBER_MODE}}
14
14
  - Text output is line-number-prefixed